Skip to content

Module

clak

Clak: A Command Line Application Kit.

Clak is a framework for building command line applications in Python. It extends and enhances Python's argparse with features like:

  • Simplified parser composition and inheritance
  • Rich command completion support
  • XDG Base Directory path flags and config-file loading (XDGConfigMixin)
  • Structured logging configuration
  • Recursive subcommand handling

Canonical public API (import from clak): - Parser: root/command class (auto-dispatches on init unless parse=False) - Argument: positional or optional argument descriptor - Arg / Opt: optional sugar for positionals vs flags (Argument still accepts both) - Command: nested subcommand descriptor (alias of SubParser)

Optional mixins (also from clak): LoggingOptMixin, RichHelpMixin, Show/List/Pprint/Raw/Markdown/Rst/Data/CompositeViewMixin, completion, XDGConfigMixin.

Secondary entry points: clak.exception, clak.views (view classes), clak.comp (mixins). Internal layout: clak.core, clak.runtime, clak.views, clak.comp. Deep module paths remain import-compatible.

Arg

Bases: Argument

Optional sugar for a positional argument.

Same *args / **kwargs as :class:Argument, but at least one positional name is required (no leading -). Empty Arg() is rejected; dest-derived flags belong on Opt / Argument. Argument still accepts both positionals and flags.

Source code in clak/core/descriptors.py
class Arg(Argument):
    """Optional sugar for a positional argument.

    Same ``*args`` / ``**kwargs`` as :class:`Argument`, but at least one
    positional name is required (no leading ``-``). Empty ``Arg()`` is
    rejected; dest-derived flags belong on ``Opt`` / ``Argument``.
    ``Argument`` still accepts both positionals and flags.
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        _check_arg_opt_names(self.args, expect_option=False, cls_name="Arg")

Argument

Bases: ArgParseItem

Represents an argument that can be added to an argument parser.

Handles both positional arguments and optional flags, choosing the appropriate argparse form from the flag names. Optional helpers :class:Arg (positionals) and :class:Opt (flags) reject mixed names.

Most keyword arguments are passed through to :meth:argparse.ArgumentParser.add_argument. Clak-only kwargs (stripped before argparse):

  • argument_group / option_group: Optional title for a help section (parser.add_argument_group). Same title reuses one section; pick the name that matches what you are grouping. Do not set both on one Argument.
  • exclusive_group: Shared key for argparse mutual exclusion (add_mutually_exclusive_group). Same key reuses one XOR set (required=False). May nest under a help section when a help-group kwarg is also set.
  • propagate: Copy this flag onto descendant parsers (default True). Set False to keep it on this parser only.
Source code in clak/core/descriptors.py
class Argument(ArgParseItem):
    """Represents an argument that can be added to an argument parser.

    Handles both positional arguments and optional flags, choosing the
    appropriate argparse form from the flag names. Optional helpers
    :class:`Arg` (positionals) and :class:`Opt` (flags) reject mixed names.

    Most keyword arguments are passed through to
    :meth:`argparse.ArgumentParser.add_argument`. Clak-only kwargs (stripped
    before argparse):

    - ``argument_group`` / ``option_group``: Optional title for a help section
      (``parser.add_argument_group``). Same title reuses one section; pick the
      name that matches what you are grouping. Do not set both on one Argument.
    - ``exclusive_group``: Shared key for argparse mutual exclusion
      (``add_mutually_exclusive_group``). Same key reuses one XOR set
      (``required=False``). May nest under a help section when a help-group
      kwarg is also set.
    - ``propagate``: Copy this flag onto descendant parsers (default True).
      Set False to keep it on this parser only.
    """

    def propagates(self) -> bool:
        """True when this flag should be copied onto descendant parsers."""
        return bool(self.kwargs.get("propagate", True))

    def flag_strings(self, dest: str) -> tuple[str, ...]:
        """Option strings for a flag Argument, or empty if it is positional."""
        if dest.startswith("__"):
            return ()
        if self.args:
            if str(self.args[0]).startswith("-"):
                return tuple(str(name) for name in self.args)
            return ()
        if not dest:
            return ()
        if len(dest) <= 2:
            return (f"-{dest}",)
        return (f"--{dest}",)

    def suppress_attach_overrides(self, extra: Optional[dict] = None) -> dict:
        """Attach-time overrides so a copy does not overwrite an outer dest.

        Do not set required unless the source flag is required: some argparse
        actions reject the required= keyword.
        """
        overrides = {"default": argparse.SUPPRESS}
        if self.kwargs.get("required"):
            overrides["required"] = False
        if extra:
            overrides.update(extra)
        return overrides

    def attach_arg_to_parser(
        self,
        key: str,
        config: "ParserNode",
        attach_overrides: Optional[dict] = None,
    ) -> argparse.Action:
        """Create and add an argument to the parser.

        Args:
            key (str): The argument key/name
            config (ParserNode): The parser configuration object
            attach_overrides (dict): Kwargs applied only to this attach
                (not stored on the shared Argument descriptor).

        Returns:
            argparse.Action: The created argument parser action
        """
        parser = config.parser
        args, kwargs = self.build_params(key)
        kwargs = dict(kwargs)
        if attach_overrides:
            kwargs.update(attach_overrides)
        if not isinstance(args, tuple):
            raise TypeError(
                f"Args must be a tuple for {self.__class__.__name__}: {type(args)}"
            )

        help_group_title = _help_group_title_from_kwargs(key, kwargs)
        exclusive_key = kwargs.pop("exclusive_group", None)
        kwargs.pop("propagate", None)

        # Create argument
        logger.debug(
            "Create new argument %s.%s: %s",
            config.get_fname(attr="key"),
            key,
            self.kwargs,
        )

        target = _parser_add_target(parser, help_group_title, exclusive_key)
        action = target.add_argument(*args, **_kwargs_for_add_argument(kwargs, parser))
        help_args = getattr(config, "help_args", None)
        if help_args is not None:
            help_args.append(HelpArg.from_action(action, group=help_group_title))
        return action

attach_arg_to_parser(key, config, attach_overrides=None)

Create and add an argument to the parser.

Parameters:

Name Type Description Default
key str

The argument key/name

required
config ParserNode

The parser configuration object

required
attach_overrides dict

Kwargs applied only to this attach (not stored on the shared Argument descriptor).

None

Returns:

Type Description
Action

argparse.Action: The created argument parser action

Source code in clak/core/descriptors.py
def attach_arg_to_parser(
    self,
    key: str,
    config: "ParserNode",
    attach_overrides: Optional[dict] = None,
) -> argparse.Action:
    """Create and add an argument to the parser.

    Args:
        key (str): The argument key/name
        config (ParserNode): The parser configuration object
        attach_overrides (dict): Kwargs applied only to this attach
            (not stored on the shared Argument descriptor).

    Returns:
        argparse.Action: The created argument parser action
    """
    parser = config.parser
    args, kwargs = self.build_params(key)
    kwargs = dict(kwargs)
    if attach_overrides:
        kwargs.update(attach_overrides)
    if not isinstance(args, tuple):
        raise TypeError(
            f"Args must be a tuple for {self.__class__.__name__}: {type(args)}"
        )

    help_group_title = _help_group_title_from_kwargs(key, kwargs)
    exclusive_key = kwargs.pop("exclusive_group", None)
    kwargs.pop("propagate", None)

    # Create argument
    logger.debug(
        "Create new argument %s.%s: %s",
        config.get_fname(attr="key"),
        key,
        self.kwargs,
    )

    target = _parser_add_target(parser, help_group_title, exclusive_key)
    action = target.add_argument(*args, **_kwargs_for_add_argument(kwargs, parser))
    help_args = getattr(config, "help_args", None)
    if help_args is not None:
        help_args.append(HelpArg.from_action(action, group=help_group_title))
    return action

flag_strings(dest)

Option strings for a flag Argument, or empty if it is positional.

Source code in clak/core/descriptors.py
def flag_strings(self, dest: str) -> tuple[str, ...]:
    """Option strings for a flag Argument, or empty if it is positional."""
    if dest.startswith("__"):
        return ()
    if self.args:
        if str(self.args[0]).startswith("-"):
            return tuple(str(name) for name in self.args)
        return ()
    if not dest:
        return ()
    if len(dest) <= 2:
        return (f"-{dest}",)
    return (f"--{dest}",)

propagates()

True when this flag should be copied onto descendant parsers.

Source code in clak/core/descriptors.py
def propagates(self) -> bool:
    """True when this flag should be copied onto descendant parsers."""
    return bool(self.kwargs.get("propagate", True))

suppress_attach_overrides(extra=None)

Attach-time overrides so a copy does not overwrite an outer dest.

Do not set required unless the source flag is required: some argparse actions reject the required= keyword.

Source code in clak/core/descriptors.py
def suppress_attach_overrides(self, extra: Optional[dict] = None) -> dict:
    """Attach-time overrides so a copy does not overwrite an outer dest.

    Do not set required unless the source flag is required: some argparse
    actions reject the required= keyword.
    """
    overrides = {"default": argparse.SUPPRESS}
    if self.kwargs.get("required"):
        overrides["required"] = False
    if extra:
        overrides.update(extra)
    return overrides

CompCmdRender

Bases: CompRenderCmdMixin, Parser

Command completion renderer class.

Combines the CompRenderCmdMixin with the base Parser to create a class that can render command completion code. This class provides the core functionality for generating shell completion scripts for command-line tools.

Key features: - Generates shell completion code for bash/tcsh/fish - Supports external completion scripts - Configurable executable names - Default completion behavior

Source code in clak/comp/completion.py
class CompCmdRender(CompRenderCmdMixin, Parser):
    """Command completion renderer class.

    Combines the CompRenderCmdMixin with the base Parser to create a class that can
    render command completion code. This class provides the core functionality for
    generating shell completion scripts for command-line tools.

    Key features:
    - Generates shell completion code for bash/tcsh/fish
    - Supports external completion scripts
    - Configurable executable names
    - Default completion behavior
    """

CompRenderCmdMixin

Bases: CompRenderMixin

Completion command support

Source code in clak/comp/completion.py
class CompRenderCmdMixin(CompRenderMixin):
    "Completion command support"

    use_defaults = Argument(
        "--no-defaults",
        action="store_false",
        default=True,
        help="when no matches are generated, do not fallback to readline's"
        + " default completion (affects bash only)",
    )
    complete_arguments = Argument(
        "--complete-arguments",
        nargs=argparse.REMAINDER,
        help="arguments to call complete with; use of this option discards default"
        + " options (affects bash only)",
    )
    shell = Argument(
        "-s",
        "--shell",
        choices=("bash", "zsh", "tcsh", "fish", "powershell"),
        default="bash",
        help="output code for the specified shell",
    )
    external_argcomplete_script = Argument(
        "-e",
        "--external-argcomplete-script",
        help=argparse.SUPPRESS,
    )
    executable = Argument(
        "--executable",
        nargs="+",
        help=argparse.SUPPRESS,
        default=None,
    )

    def cli_run(self, ctx, **kwargs):  # pylint: disable=unused-argument
        """Command completion support mixin.

        Adds command completion support to parsers by providing arguments to configure
        shell completion behavior:

        - --no-defaults: Disable fallback to readline defaults (bash only)
        - --complete-arguments: Custom completion arguments (bash only)
        - --shell: Target shell (bash, zsh, tcsh, fish, powershell)
        - --executable: Name of executable to complete

        The mixin generates the appropriate shell completion code when run.
        Supports bash (default), zsh, tcsh, fish and powershell shells.

        Example:
            my-app completion  # Outputs bash completion code
            my-app completion --shell zsh  # Outputs zsh completion code
        """

        args = ctx.args
        executable = getattr(args, "executable", None)
        if not executable:
            prog = getattr(ctx, "app_proc_name", None) or getattr(
                ctx.cli_root, "proc_name", None
            )
            if not prog:
                prog = sys.argv[0]
            args.executable = [prog]
        self.print_completion_stdout(args)

cli_run(ctx, **kwargs)

Command completion support mixin.

Adds command completion support to parsers by providing arguments to configure shell completion behavior:

  • --no-defaults: Disable fallback to readline defaults (bash only)
  • --complete-arguments: Custom completion arguments (bash only)
  • --shell: Target shell (bash, zsh, tcsh, fish, powershell)
  • --executable: Name of executable to complete

The mixin generates the appropriate shell completion code when run. Supports bash (default), zsh, tcsh, fish and powershell shells.

Example

my-app completion # Outputs bash completion code my-app completion --shell zsh # Outputs zsh completion code

Source code in clak/comp/completion.py
def cli_run(self, ctx, **kwargs):  # pylint: disable=unused-argument
    """Command completion support mixin.

    Adds command completion support to parsers by providing arguments to configure
    shell completion behavior:

    - --no-defaults: Disable fallback to readline defaults (bash only)
    - --complete-arguments: Custom completion arguments (bash only)
    - --shell: Target shell (bash, zsh, tcsh, fish, powershell)
    - --executable: Name of executable to complete

    The mixin generates the appropriate shell completion code when run.
    Supports bash (default), zsh, tcsh, fish and powershell shells.

    Example:
        my-app completion  # Outputs bash completion code
        my-app completion --shell zsh  # Outputs zsh completion code
    """

    args = ctx.args
    executable = getattr(args, "executable", None)
    if not executable:
        prog = getattr(ctx, "app_proc_name", None) or getattr(
            ctx.cli_root, "proc_name", None
        )
        if not prog:
            prog = sys.argv[0]
        args.executable = [prog]
    self.print_completion_stdout(args)

CompRenderOptMixin

Bases: CompRenderMixin

Completion options support mixin.

Adds a --completion flag that prints bash argcomplete shellcode instead of running the command. Prefer :class:CompCmdRender as a completion subcommand when you need --shell / --executable.

Example::

my-app --completion
Source code in clak/comp/completion.py
class CompRenderOptMixin(CompRenderMixin):
    """Completion options support mixin.

    Adds a ``--completion`` flag that prints bash argcomplete shellcode
    instead of running the command. Prefer :class:`CompCmdRender` as a
    ``completion`` subcommand when you need ``--shell`` / ``--executable``.

    Example::

        my-app --completion
    """

    completion_cmd = Argument(
        "--completion",
        action="store_true",
        help="output code for the specified shell",
    )

    def cli_run(self, ctx, **kwargs):
        """Print bash completion shellcode when ``--completion`` is set.

        Example::

            my-app --completion
        """

        args = ctx.args

        prog = getattr(ctx, "app_proc_name", None) or getattr(
            ctx.cli_root, "proc_name", None
        )
        if not prog:
            prog = sys.argv[0]

        kwargs = {
            "executable": [prog],
            "shell": "bash",
            "use_defaults": True,
            "complete_arguments": [],
            "external_argcomplete_script": None,
        }
        if args.completion_cmd is True:
            self.print_completion_stdout(SimpleNamespace(**kwargs))
        else:
            super().cli_run(ctx, **kwargs)

cli_run(ctx, **kwargs)

Print bash completion shellcode when --completion is set.

Example::

my-app --completion
Source code in clak/comp/completion.py
def cli_run(self, ctx, **kwargs):
    """Print bash completion shellcode when ``--completion`` is set.

    Example::

        my-app --completion
    """

    args = ctx.args

    prog = getattr(ctx, "app_proc_name", None) or getattr(
        ctx.cli_root, "proc_name", None
    )
    if not prog:
        prog = sys.argv[0]

    kwargs = {
        "executable": [prog],
        "shell": "bash",
        "use_defaults": True,
        "complete_arguments": [],
        "external_argcomplete_script": None,
    }
    if args.completion_cmd is True:
        self.print_completion_stdout(SimpleNamespace(**kwargs))
    else:
        super().cli_run(ctx, **kwargs)

CompositeViewMixin

Bases: TextLayoutOptMixin, TableViewOptMixin

CLI flags for multi-section :class:~clak.views.CompositeView output.

Adds table options, --expand-keys, --format-scope, --width, and --line-length. Does not set Meta.cli_view: return a CompositeView(...) from cli_run. Table flags apply to the primary section only. --line-length applies to text/pprint sections only. --expand-keys is for a ListView primary; hide it with Meta.view_cli_options when the primary is ShowView. --format is table-scoped (view / yaml / json / csv); markdown source is in --format-scope all envelopes, not --format raw.

Source code in clak/comp/views.py
class CompositeViewMixin(TextLayoutOptMixin, TableViewOptMixin):
    """CLI flags for multi-section :class:`~clak.views.CompositeView` output.

    Adds table options, ``--expand-keys``, ``--format-scope``, ``--width``,
    and ``--line-length``. Does **not** set ``Meta.cli_view``: return a
    ``CompositeView(...)`` from ``cli_run``. Table flags apply to the primary
    section only. ``--line-length`` applies to text/pprint sections only.
    ``--expand-keys`` is for a ListView primary; hide it with
    ``Meta.view_cli_options`` when the primary is ShowView.
    ``--format`` is table-scoped (``view`` / ``yaml`` / ``json`` / ``csv``);
    markdown source is in ``--format-scope all`` envelopes, not ``--format raw``.
    """

    _view_cli_option_names = (
        _LAYER_TABLE_DESTS
        | _LAYER_LIST_DESTS
        | _LAYER_COMPOSITE_DESTS
        | _LAYER_TEXT_LAYOUT_DESTS
    )

    meta__config__view_format_scope = MetaSetting(
        help="Default format scope for CompositeView: first or all",
    )
    meta__view_format_scope = None

    meta__config__view_expand_keys = MetaSetting(
        help="Default for --expand-keys / --no-expand-keys",
    )
    meta__view_expand_keys = None

    expand_keys = Argument(
        "--expand-keys",
        action=argparse.BooleanOptionalAction,
        default=None,
        option_group=_OUTPUT_OPTIONS_GROUP,
        help=_EXPAND_KEYS_HELP,
    )
    format_scope = Argument(
        "--format-scope",
        choices=sorted(FORMAT_SCOPES),
        default=None,
        option_group=_OUTPUT_OPTIONS_GROUP,
        help=_FORMAT_SCOPE_HELP,
    )

DataViewMixin

Bases: _ViewMixinBase

Auto-render command results with :class:~clak.views.DataView.

Adds --format (json / yaml), --compact / --no-compact, --color / --no-color, and --anchors / --no-anchors. Syntax theme: Meta.view_syntax_theme or CLAK_SYNTAX_THEME, else ansi_dark. Configure exposed flags with Meta.view_cli_options.

Source code in clak/comp/views.py
class DataViewMixin(_ViewMixinBase):
    """Auto-render command results with :class:`~clak.views.DataView`.

    Adds ``--format`` (``json`` / ``yaml``), ``--compact`` / ``--no-compact``,
    ``--color`` / ``--no-color``, and ``--anchors`` / ``--no-anchors``.
    Syntax theme: ``Meta.view_syntax_theme`` or ``CLAK_SYNTAX_THEME``, else
    ``ansi_dark``. Configure exposed flags with ``Meta.view_cli_options``.
    """

    _view_cli_option_names = _LAYER_DATA_DESTS
    _uses_syntax_theme = True
    meta__cli_view = DataView

    meta__config__view_format = MetaSetting(
        help="Default data format: json, yaml, or unset for auto",
    )
    meta__view_format = None

    meta__config__view_compact = MetaSetting(
        help="Default for --compact / --no-compact (JSON only)",
    )
    meta__view_compact = None

    meta__config__view_color = MetaSetting(
        help="Default for --color / --no-color",
    )
    meta__view_color = None

    meta__config__view_anchors = MetaSetting(
        help="Default for --anchors / --no-anchors (YAML only)",
    )
    meta__view_anchors = None

    meta__config__view_syntax_theme = MetaSetting(
        help=(
            "Pygments/Rich Syntax theme for DataView and Markdown code. "
            "Overrides CLAK_SYNTAX_THEME; default ansi_dark"
        ),
    )
    meta__view_syntax_theme = None

    format = Argument(
        "--format",
        choices=sorted(DATA_FORMATS),
        default=None,
        option_group=_OUTPUT_OPTIONS_GROUP,
        help=_DATA_FORMAT_HELP,
    )

    compact = Argument(
        "--compact",
        action=argparse.BooleanOptionalAction,
        default=None,
        option_group=_OUTPUT_OPTIONS_GROUP,
        help=_COMPACT_HELP,
    )

    color = Argument(
        "--color",
        action=argparse.BooleanOptionalAction,
        default=None,
        option_group=_OUTPUT_OPTIONS_GROUP,
        help=_COLOR_HELP,
    )

    anchors = Argument(
        "--anchors",
        action=argparse.BooleanOptionalAction,
        default=None,
        option_group=_OUTPUT_OPTIONS_GROUP,
        help=_ANCHORS_HELP,
    )

ListViewMixin

Bases: TableViewOptMixin

Auto-render command results with :class:~clak.views.ListView.

Adds --columns, --add-index / --no-add-index, --expand-keys / --no-expand-keys, --format, --sort-columns, --sort-mode, --width, and --wrap. Configure exposed flags with Meta.view_cli_options.

Source code in clak/comp/views.py
class ListViewMixin(TableViewOptMixin):
    """Auto-render command results with :class:`~clak.views.ListView`.

    Adds ``--columns``, ``--add-index`` / ``--no-add-index``,
    ``--expand-keys`` / ``--no-expand-keys``, ``--format``,
    ``--sort-columns``, ``--sort-mode``, ``--width``, and ``--wrap``.
    Configure exposed flags with ``Meta.view_cli_options``.
    """

    _view_cli_option_names = _LAYER_TABLE_DESTS | _LAYER_LIST_DESTS
    meta__cli_view = ListView

    meta__config__view_expand_keys = MetaSetting(
        help="Default for --expand-keys / --no-expand-keys",
    )
    meta__view_expand_keys = None

    expand_keys = Argument(
        "--expand-keys",
        action=argparse.BooleanOptionalAction,
        default=None,
        option_group=_OUTPUT_OPTIONS_GROUP,
        help=_EXPAND_KEYS_HELP,
    )

LoggingOptMixin

Bases: PluginHelpers

Logging options support

Source code in clak/comp/logging.py
class LoggingOptMixin(PluginHelpers):
    "Logging options support"

    verbosity = Argument(
        "-v",
        "--verbose",
        action="count",
        default=0,
        help="Increase verbosity level (-v, -vv, -vvv, -vvvv)",
    )

    log_format = Argument(
        "--log-format",
        choices=["default", "extended", "audit", "debug"],
        help="Set log formatter",
        default="default",
    )

    app_trace_mode = Argument(
        "--trace",
        default=False,
        action=argparse.BooleanOptionalAction,
        help="Enable trace logging on errors",
    )

    log_colors = Argument(
        "--log-colors",
        default=None,
        action=argparse.BooleanOptionalAction,
        help=(
            "Enable colored logs (default: on for TTY; "
            "override with {log_colors_env} or this flag)"
        ),
    )

    # Meta settings
    meta__config__log_prefix = MetaSetting(
        help=(
            "Base name for self.logger, usually __name__. "
            "If omitted, the parser module name is used."
        ),
    )
    meta__config__log_suffix = MetaSetting(
        help="Suffix of the logger name, override the right part.",
    )
    meta__config__log_default_level = MetaSetting(
        help="Default log level of the logger, usually WARNING, INFO or DEBUG",
    )

    meta__config__log_levels = MetaSetting(
        help="List of log levels to use, usually INFO and DEBUG",
    )

    meta__config__log_silent = MetaSetting(
        help="List of loggers to silent, usually too verbose loggers",
    )

    meta__config__log_colors_env = MetaSetting(
        help=(
            "Env var name for --log-colors default (help text + resolve). "
            f"Default: {DEFAULT_LOG_COLORS_ENV}"
        ),
    )

    logger = None

    def add_arguments(self, arguments: dict = None):
        """Format ``--log-colors`` help with ``Meta.log_colors_env``, then register."""
        if arguments is None:
            arguments = getattr(self, "meta__arguments_dict", None)
        arguments = dict(arguments or {})
        env_name = self.query_cfg_parents(
            "log_colors_env", default=DEFAULT_LOG_COLORS_ENV, include_self=True
        )
        if not env_name:
            env_name = DEFAULT_LOG_COLORS_ENV

        template = getattr(type(self), "log_colors", None)
        if isinstance(template, Argument) and "log_colors" not in arguments:
            kwargs = dict(template.kwargs)
            help_text = kwargs.get("help")
            if isinstance(help_text, str) and "{log_colors_env}" in help_text:
                kwargs["help"] = help_text.format(log_colors_env=env_name)
            arg = Argument(*template.args, **kwargs)
            arg.destination = "log_colors"
            arguments["log_colors"] = arg

        return super().add_arguments(arguments)

    @staticmethod
    def _log_level(value):
        "Return a validated numeric logging level."
        if isinstance(value, int):
            return value
        if not isinstance(value, str):
            raise TypeError(f"Log level must be a string or integer, got {type(value)}")
        # logging has no public name→level map on all supported Pythons.
        # pylint: disable-next=protected-access
        name_to_level = logging._nameToLevel
        level = name_to_level.get(value.upper())
        if level is None:
            choices = ", ".join(sorted(name_to_level))
            raise ValueError(f"Unknown log level '{value}', choose one of: {choices}")
        return level

    def assemble_user_config(self, configs):
        """Build cumulative verbosity tiers from ``Meta.log_levels``.

        Explicit entries use ``LEVEL|logger``. Legacy configurations containing
        only logger names are expanded to INFO and DEBUG tiers for each group.
        """
        if not isinstance(configs, list) or not configs:
            raise ValueError("log_levels must be a non-empty list of lists")
        if not all(isinstance(tier, list) for tier in configs):
            raise TypeError("Each log_levels tier must be a list")

        entries = [entry for tier in configs for entry in tier]
        if not all(isinstance(entry, str) for entry in entries):
            raise TypeError("Each log_levels entry must be a string")

        legacy = all("|" not in entry for entry in entries)
        if legacy:
            configs = [
                [f"{logging.getLevelName(level)}|{logger_name}" for logger_name in tier]
                for tier in configs
                for level in (logging.INFO, logging.DEBUG)
            ]

        user_config = []
        for idx, lvl_config in enumerate(configs):
            final = []
            for entry in lvl_config:
                if "|" in entry:
                    level_name, logger_name = entry.split("|", maxsplit=1)
                    level = self._log_level(level_name)
                else:
                    logger_name = entry
                    level = (logging.INFO, logging.DEBUG)[idx % 2]
                final.append(
                    SimpleNamespace(
                        logger_name=logger_name,
                        level=level,
                    )
                )
            user_config.append(final)
        return user_config

    def select_user_config(self, user_config, req=0):
        "Select user config from requested level"
        max_level = len(user_config) - 1
        if not isinstance(req, int) or req < 0 or req > max_level:
            raise ClakAppError(
                f"Verbosity must be between 0 and {max_level}, got {req}"
            )

        req_config = {}
        for idx, logger_config in enumerate(user_config):
            if idx > req:
                break

            for logger_ns in logger_config:
                req_config[logger_ns.logger_name] = _logger_entry(level=logger_ns.level)

        return SimpleNamespace(
            config=req_config,
            max_level=max_level,
            level=req,
        )

    def cli_hook__logging(  # pylint: disable=too-many-locals,too-many-branches,too-many-statements
        self, instance, ctx, **_
    ):
        "Inject or create logger into instance"

        logger.debug("Load Logging hook for %s", instance)

        log_prefix = self.query_cfg_parents(
            "log_prefix", default=None, include_self=True
        )
        log_suffix = self.query_cfg_parents(
            "log_suffix", default=None, include_self=True
        )

        if ctx.cli_first:
            log_levels = self.query_cfg_parents(
                "log_levels", default=None, include_self=True
            )
            log_silent = self.query_cfg_parents(
                "log_silent", default=None, include_self=True
            )
            log_default_level = self.query_cfg_parents(
                "log_default_level", default=DEFAULT_LOG_LEVEL, include_self=True
            )
            if log_default_level is None:
                log_default_level = DEFAULT_LOG_LEVEL
            log_verbosity = ctx.args.verbosity
            log_colors_env = self.query_cfg_parents(
                "log_colors_env",
                default=DEFAULT_LOG_COLORS_ENV,
                include_self=True,
            )
            if not log_colors_env:
                log_colors_env = DEFAULT_LOG_COLORS_ENV
            raw_log_colors = os.environ.get(log_colors_env)
            env_log_colors = (
                to_boolean(raw_log_colors) if raw_log_colors is not None else None
            )
            log_colors = resolve_log_colors(
                ctx.args.get("log_colors"), env_value=env_log_colors
            )

            log_silent = log_silent or []
            if not isinstance(log_silent, list) or not all(
                isinstance(name, str) for name in log_silent
            ):
                raise TypeError("log_silent must be a list of logger names")

            user_config = self.assemble_user_config(log_levels or DEFAULT_LOG_LEVELS)
            log_config = self.select_user_config(user_config, req=log_verbosity)

            logger_config = {
                "": _logger_entry(level=self._log_level(log_default_level)),
            }
            logger_config.update(log_config.config)

            logs_silenced = log_verbosity < log_config.max_level
            if logs_silenced:
                for logger_name in log_silent:
                    logger_config[logger_name] = _logger_entry(level=logging.WARNING)

            get_app_logger(
                loggers=logger_config,
                level=logging.NOTSET,
                formatter=ctx.args.log_format,
                colors=log_colors,
            )

            logger.info(
                "Logging set to %s/%s",
                log_verbosity,
                log_config.max_level,
            )
            for name, conf in logger_config.items():
                logger.info("  %s: %s", logging.getLevelName(conf["level"]), name)
            if log_silent:
                if logs_silenced:
                    logger.info("Logging to WARNING: %s", ", ".join(log_silent))
                else:
                    logger.info("All configured logs are shown")

        # Create internal logger instance if not already created
        if log_suffix is None:
            log_suffix = "==FLAT=="

        if log_suffix == argparse.SUPPRESS:
            suffix = ""
        elif log_suffix == "==FLAT==":
            suffix = f".{instance.__class__.__name__}"
        elif log_suffix == "==NESTED==":
            suffix = _dotted_suffix(instance.get_fname(attr="key"))
        else:
            suffix = _dotted_suffix(log_suffix)

        log_name = instance.__class__.__module__
        if log_prefix is not None:
            log_name = f"{log_prefix}{suffix}"
        instance.logger = logging.getLogger(log_name)
        logger.debug("Enable logging for '%s': %s", instance, log_name)

        logger.debug("Logging hook loaded for %s", instance)

        ctx.plugins.update(
            {
                "log_acquired_root_logger": True,
                "log_prefix": log_prefix,
                "log_suffix_req": log_suffix,
                "log_suffix": suffix,
            }
        )

    def test_logger(self, instance: object | None = None) -> None:
        """Test the logger by sending test messages at different log levels.

        Args:
            instance: The instance to test logging for. If None, uses self.
        """
        instance = instance if instance else self
        instance.logger.debug("Test logger with DEBUG")
        instance.logger.info("Test logger with INFO")
        instance.logger.warning("Test logger with WARNING")
        instance.logger.error("Test logger with ERROR")
        instance.logger.critical("Test logger with CRITICAL")

add_arguments(arguments=None)

Format --log-colors help with Meta.log_colors_env, then register.

Source code in clak/comp/logging.py
def add_arguments(self, arguments: dict = None):
    """Format ``--log-colors`` help with ``Meta.log_colors_env``, then register."""
    if arguments is None:
        arguments = getattr(self, "meta__arguments_dict", None)
    arguments = dict(arguments or {})
    env_name = self.query_cfg_parents(
        "log_colors_env", default=DEFAULT_LOG_COLORS_ENV, include_self=True
    )
    if not env_name:
        env_name = DEFAULT_LOG_COLORS_ENV

    template = getattr(type(self), "log_colors", None)
    if isinstance(template, Argument) and "log_colors" not in arguments:
        kwargs = dict(template.kwargs)
        help_text = kwargs.get("help")
        if isinstance(help_text, str) and "{log_colors_env}" in help_text:
            kwargs["help"] = help_text.format(log_colors_env=env_name)
        arg = Argument(*template.args, **kwargs)
        arg.destination = "log_colors"
        arguments["log_colors"] = arg

    return super().add_arguments(arguments)

assemble_user_config(configs)

Build cumulative verbosity tiers from Meta.log_levels.

Explicit entries use LEVEL|logger. Legacy configurations containing only logger names are expanded to INFO and DEBUG tiers for each group.

Source code in clak/comp/logging.py
def assemble_user_config(self, configs):
    """Build cumulative verbosity tiers from ``Meta.log_levels``.

    Explicit entries use ``LEVEL|logger``. Legacy configurations containing
    only logger names are expanded to INFO and DEBUG tiers for each group.
    """
    if not isinstance(configs, list) or not configs:
        raise ValueError("log_levels must be a non-empty list of lists")
    if not all(isinstance(tier, list) for tier in configs):
        raise TypeError("Each log_levels tier must be a list")

    entries = [entry for tier in configs for entry in tier]
    if not all(isinstance(entry, str) for entry in entries):
        raise TypeError("Each log_levels entry must be a string")

    legacy = all("|" not in entry for entry in entries)
    if legacy:
        configs = [
            [f"{logging.getLevelName(level)}|{logger_name}" for logger_name in tier]
            for tier in configs
            for level in (logging.INFO, logging.DEBUG)
        ]

    user_config = []
    for idx, lvl_config in enumerate(configs):
        final = []
        for entry in lvl_config:
            if "|" in entry:
                level_name, logger_name = entry.split("|", maxsplit=1)
                level = self._log_level(level_name)
            else:
                logger_name = entry
                level = (logging.INFO, logging.DEBUG)[idx % 2]
            final.append(
                SimpleNamespace(
                    logger_name=logger_name,
                    level=level,
                )
            )
        user_config.append(final)
    return user_config

cli_hook__logging(instance, ctx, **_)

Inject or create logger into instance

Source code in clak/comp/logging.py
def cli_hook__logging(  # pylint: disable=too-many-locals,too-many-branches,too-many-statements
    self, instance, ctx, **_
):
    "Inject or create logger into instance"

    logger.debug("Load Logging hook for %s", instance)

    log_prefix = self.query_cfg_parents(
        "log_prefix", default=None, include_self=True
    )
    log_suffix = self.query_cfg_parents(
        "log_suffix", default=None, include_self=True
    )

    if ctx.cli_first:
        log_levels = self.query_cfg_parents(
            "log_levels", default=None, include_self=True
        )
        log_silent = self.query_cfg_parents(
            "log_silent", default=None, include_self=True
        )
        log_default_level = self.query_cfg_parents(
            "log_default_level", default=DEFAULT_LOG_LEVEL, include_self=True
        )
        if log_default_level is None:
            log_default_level = DEFAULT_LOG_LEVEL
        log_verbosity = ctx.args.verbosity
        log_colors_env = self.query_cfg_parents(
            "log_colors_env",
            default=DEFAULT_LOG_COLORS_ENV,
            include_self=True,
        )
        if not log_colors_env:
            log_colors_env = DEFAULT_LOG_COLORS_ENV
        raw_log_colors = os.environ.get(log_colors_env)
        env_log_colors = (
            to_boolean(raw_log_colors) if raw_log_colors is not None else None
        )
        log_colors = resolve_log_colors(
            ctx.args.get("log_colors"), env_value=env_log_colors
        )

        log_silent = log_silent or []
        if not isinstance(log_silent, list) or not all(
            isinstance(name, str) for name in log_silent
        ):
            raise TypeError("log_silent must be a list of logger names")

        user_config = self.assemble_user_config(log_levels or DEFAULT_LOG_LEVELS)
        log_config = self.select_user_config(user_config, req=log_verbosity)

        logger_config = {
            "": _logger_entry(level=self._log_level(log_default_level)),
        }
        logger_config.update(log_config.config)

        logs_silenced = log_verbosity < log_config.max_level
        if logs_silenced:
            for logger_name in log_silent:
                logger_config[logger_name] = _logger_entry(level=logging.WARNING)

        get_app_logger(
            loggers=logger_config,
            level=logging.NOTSET,
            formatter=ctx.args.log_format,
            colors=log_colors,
        )

        logger.info(
            "Logging set to %s/%s",
            log_verbosity,
            log_config.max_level,
        )
        for name, conf in logger_config.items():
            logger.info("  %s: %s", logging.getLevelName(conf["level"]), name)
        if log_silent:
            if logs_silenced:
                logger.info("Logging to WARNING: %s", ", ".join(log_silent))
            else:
                logger.info("All configured logs are shown")

    # Create internal logger instance if not already created
    if log_suffix is None:
        log_suffix = "==FLAT=="

    if log_suffix == argparse.SUPPRESS:
        suffix = ""
    elif log_suffix == "==FLAT==":
        suffix = f".{instance.__class__.__name__}"
    elif log_suffix == "==NESTED==":
        suffix = _dotted_suffix(instance.get_fname(attr="key"))
    else:
        suffix = _dotted_suffix(log_suffix)

    log_name = instance.__class__.__module__
    if log_prefix is not None:
        log_name = f"{log_prefix}{suffix}"
    instance.logger = logging.getLogger(log_name)
    logger.debug("Enable logging for '%s': %s", instance, log_name)

    logger.debug("Logging hook loaded for %s", instance)

    ctx.plugins.update(
        {
            "log_acquired_root_logger": True,
            "log_prefix": log_prefix,
            "log_suffix_req": log_suffix,
            "log_suffix": suffix,
        }
    )

select_user_config(user_config, req=0)

Select user config from requested level

Source code in clak/comp/logging.py
def select_user_config(self, user_config, req=0):
    "Select user config from requested level"
    max_level = len(user_config) - 1
    if not isinstance(req, int) or req < 0 or req > max_level:
        raise ClakAppError(
            f"Verbosity must be between 0 and {max_level}, got {req}"
        )

    req_config = {}
    for idx, logger_config in enumerate(user_config):
        if idx > req:
            break

        for logger_ns in logger_config:
            req_config[logger_ns.logger_name] = _logger_entry(level=logger_ns.level)

    return SimpleNamespace(
        config=req_config,
        max_level=max_level,
        level=req,
    )

test_logger(instance=None)

Test the logger by sending test messages at different log levels.

Parameters:

Name Type Description Default
instance object | None

The instance to test logging for. If None, uses self.

None
Source code in clak/comp/logging.py
def test_logger(self, instance: object | None = None) -> None:
    """Test the logger by sending test messages at different log levels.

    Args:
        instance: The instance to test logging for. If None, uses self.
    """
    instance = instance if instance else self
    instance.logger.debug("Test logger with DEBUG")
    instance.logger.info("Test logger with INFO")
    instance.logger.warning("Test logger with WARNING")
    instance.logger.error("Test logger with ERROR")
    instance.logger.critical("Test logger with CRITICAL")

MarkdownViewMixin

Bases: TextViewOptMixin

Auto-render command results with :class:~clak.views.MarkdownView.

Adds --format (view / raw) and --line-length. Syntax theme: Meta.view_syntax_theme or CLAK_SYNTAX_THEME, else ansi_dark. Configure exposed flags with Meta.view_cli_options.

Source code in clak/comp/views.py
class MarkdownViewMixin(TextViewOptMixin):
    """Auto-render command results with :class:`~clak.views.MarkdownView`.

    Adds ``--format`` (``view`` / ``raw``) and ``--line-length``.
    Syntax theme: ``Meta.view_syntax_theme`` or ``CLAK_SYNTAX_THEME``, else
    ``ansi_dark``. Configure exposed flags with ``Meta.view_cli_options``.
    """

    _view_cli_option_names = _LAYER_TEXT_LAYOUT_DESTS | _LAYER_TEXT_DESTS
    _uses_syntax_theme = True
    meta__cli_view = MarkdownView

    meta__config__view_syntax_theme = MetaSetting(
        help=(
            "Pygments/Rich Syntax theme for markdown code fences. "
            "Overrides CLAK_SYNTAX_THEME; default ansi_dark"
        ),
    )
    meta__view_syntax_theme = None

Opt

Bases: Argument

Optional sugar for an option flag.

Same *args / **kwargs as :class:Argument. When names are given, every name must start with - / --. Empty Opt() is allowed: the attribute name becomes a dest-derived flag (--attr or -x). Argument still accepts both positionals and flags.

Source code in clak/core/descriptors.py
class Opt(Argument):
    """Optional sugar for an option flag.

    Same ``*args`` / ``**kwargs`` as :class:`Argument`. When names are
    given, every name must start with ``-`` / ``--``. Empty ``Opt()`` is
    allowed: the attribute name becomes a dest-derived flag (``--attr``
    or ``-x``). ``Argument`` still accepts both positionals and flags.
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        _check_arg_opt_names(self.args, expect_option=True, cls_name="Opt")

Parser

Bases: ParserNode

A simplified parser class that extends ParserNode.

This class provides a more streamlined interface to ParserNode by: - Automatically parsing arguments on initialization - Maintaining compatibility with legacy argument parser names - Providing simpler command/argument creation methods

Parameters:

Name Type Description Default
*args Any

Positional arguments passed to ParserNode

()
parse bool

Whether to automatically parse arguments on init, only on root nodes

True
**kwargs Any

Keyword arguments passed to ParserNode

{}
Source code in clak/core/parser.py
class Parser(ParserNode):
    """A simplified parser class that extends ParserNode.

    This class provides a more streamlined interface to ParserNode by:
    - Automatically parsing arguments on initialization
    - Maintaining compatibility with legacy argument parser names
    - Providing simpler command/argument creation methods

    Args:
        *args: Positional arguments passed to ParserNode
        parse (bool): Whether to automatically parse arguments on init,
            only on root nodes
        **kwargs: Keyword arguments passed to ParserNode
    """

    def __init__(self, *args: Any, parse: bool = True, **kwargs: Any):
        super().__init__(*args, **kwargs)

        if not self.parent and parse is True:
            logger.debug("Starting automatic arg_parse")
            self.dispatch(*args)

ParserNode

Bases: Node

An extensible argument parser that can be inherited to create custom CLIs.

This class provides a framework for building complex command-line interfaces with: - Hierarchical subcommands - Automatic help generation - Plugin support - Custom argument types - Exception handling

The parser can be extended by: 1. Subclassing and adding Argument instances as class attributes 2. Adding SubParser instances to create command hierarchies 3. Implementing cli_run() for command implementation 4. Implementing cli_group() for command group behavior

Attributes:

Name Type Description
children dict

Dictionary of subcommand name to child parser

meta__name str

ParserNode name

Source code in clak/core/parser.py
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
class ParserNode(Node):  # pylint: disable=too-many-instance-attributes
    """An extensible argument parser that can be inherited to create custom CLIs.

    This class provides a framework for building complex command-line interfaces with:
    - Hierarchical subcommands
    - Automatic help generation
    - Plugin support
    - Custom argument types
    - Exception handling

    The parser can be extended by:
    1. Subclassing and adding Argument instances as class attributes
    2. Adding SubParser instances to create command hierarchies
    3. Implementing cli_run() for command implementation
    4. Implementing cli_group() for command group behavior

    Attributes:
        children (dict): Dictionary of subcommand name to child parser
        meta__name (str): ParserNode name
    """

    meta__name: str = NOT_SET

    meta__subcommands_dict: dict[str, SubParser] = {}
    meta__arguments_dict: dict[str, Argument] = {}

    meta__cli_view: ClakView = None

    # Meta settings
    meta__config__name = MetaSetting(
        help="Name of the parser",
    )
    meta__config__app_name = MetaSetting(
        help="Name of the application",
    )
    meta__config__app_proc_name = MetaSetting(
        help="Name of the application processus",
    )
    meta__config__help_usage = MetaSetting(
        help="Message to display in help usage",
    )
    meta__config__help_description = MetaSetting(
        help="Message to display in help description",
    )
    meta__config__help_epilog = MetaSetting(
        help="Message to display in help epilog",
    )
    meta__config__help_formatter = MetaSetting(
        help="argparse HelpFormatter class for --help",
    )
    meta__config__help_subcommands = MetaSetting(
        help=(
            "How --help lists subcommands: 'all' (nested children, default) "
            "or 'top' (immediate children only). Inherited; a child may override."
        ),
    )
    meta__config__help_hide_parent = MetaSetting(
        help=(
            "When listing nested subcommands, replace the parent path with "
            "spaces so only the leaf name is shown (default True)."
        ),
    )
    meta__config__command_groups = MetaSetting(
        help=(
            "Ordered (key, title) pairs for subcommand help sections. "
            "Formatter metadata only; not a second add_subparsers."
        ),
    )
    meta__config__parse_intermixed = MetaSetting(
        help=(
            "Mix this command's flags and positionals (default True). "
            "Inherited; set False for argparse leftover errors after a flag. "
            "No-op on parsers with subcommands or nargs=REMAINDER."
        ),
    )
    meta__config__propagate_options = MetaSetting(
        help=(
            "Copy eligible ancestor flags onto this parser (default True). "
            "Inherited; a child may set False. Per-flag opt-out: propagate=False."
        ),
    )
    meta__config__propagate_options_group = MetaSetting(
        help=(
            "Help section title for propagated ancestor flags "
            "(default 'parent options'). Inherited; a child may override."
        ),
    )
    meta__config__known_exceptions = MetaSetting(
        help="List of known exceptions to handle",
    )
    meta__config__exception_handlers = MetaSetting(
        help=(
            "Extra (exception_type, handler) pairs or handler callables "
            "for clean_terminate (third-party libs, etc.)"
        ),
    )

    # Views support
    meta__config__cli_view = MetaSetting(
        help="class of the view to use",
    )
    meta__config__runtime_narrow_width = MetaSetting(
        help="Column threshold for ctx.runtime.is_narrow (default 80)",
    )

    def __init__(  # pylint: disable=too-many-arguments,too-many-positional-arguments
        self,
        add_help: bool = True,
        parent: "ParserNode" = None,
        name: str = None,
        key: str = None,
        parser: argparse.ArgumentParser = None,
        proc_name: str = None,
    ):
        """Initialize the parser.

        Args:
            add_help (bool): Whether to add help flags
            parent (ParserNode): Parent parser instance
            name (str): ParserNode name
            key (str): ParserNode key
            parser (ArgumentParser): Existing parser to use
            proc_name (str): Process name
        """
        self.logger = logger

        if parent is None:
            apply_debug_logging()

        super().__init__(parent=parent)

        self.dispatcher = Dispatcher(self)
        self.terminator = Terminator(self)
        self.help_args = []
        self.command_group = None
        self.command_help = None

        self.name = self.query_cfg_parents("name", default=self.__class__.__name__)
        self.key = key
        self.fkey = self.get_fname(attr="key")
        self.proc_name = proc_name
        self.add_help = add_help

        # Add children link
        self.children = {}
        self.registry = {}
        if parent:
            parent.children[self.key] = self
            self.registry = parent.registry
        self.registry[self.fkey] = self

        # Create or reuse parent parser
        if parser is None:
            self.parser = self.create_parser()
            self.proc_name = self.parser.prog
        else:
            self.parser = parser
            self.proc_name = self.parent.proc_name
            # add_parser() builds ArgumentParserPlus without clak_instance.
            if hasattr(self.parser, "clak_instance"):
                self.parser.clak_instance = self

        # Init _subparsers
        self._subparsers = None
        self.local_flag_arguments: dict[str, Argument] = {}

        self.add_arguments()
        self.add_subcommands()
        self._cli_hooks = self._collect_cli_hooks()
        self._install_help()

    def __repr__(self):
        return f"<{self.__class__.__module__}.{self.__class__.__name__}>"

    def create_parser(self):
        "Create a new parser"
        usage = self.query_cfg_parents("help_usage", default=None)
        desc = self.query_cfg_parents("help_description", default=self.__doc__)
        epilog = self.query_cfg_parents("help_epilog", default=None)

        fenv = FormatEnv({"self": self})
        usage = prepare_docstring(usage, variables=fenv.get())
        desc = prepare_docstring(desc, variables=fenv.get())
        epilog = prepare_docstring(epilog, variables=fenv.get())
        parser = ArgumentParserPlus(
            prog=self.proc_name,
            usage=usage,
            description=desc,
            epilog=epilog,
            formatter_class=self.get_help_formatter_class(),
            add_help=self.add_help,
            exit_on_error=False,
            clak_instance=self,
            parse_intermixed=self.query_cfg_parents(
                "parse_intermixed", default=True, include_self=True
            ),
        )
        return parser

    def get_help_formatter_class(self):
        """Return the argparse HelpFormatter class for this node.

        Mixins set ``meta__help_formatter`` (or ``Meta.help_formatter``).
        Unset walks parents, then ``RichRecursiveHelpFormatter``.
        Opt out with ``Meta.help_formatter = RecursiveHelpFormatter``.
        """
        from clak.comp.help import (  # pylint: disable=import-outside-toplevel
            RichRecursiveHelpFormatter,
        )

        return self.query_cfg_parents(
            "help_formatter",
            default=RichRecursiveHelpFormatter,
            include_self=True,
        )

    def _install_help(self):
        """Attach HelpRenderer and version-stable parse flags to the wrapper."""
        from clak.comp.help import (  # pylint: disable=import-outside-toplevel
            RichRecursiveHelpFormatter,
            help_document_colorizer,
        )

        self.help_layout = HelpLayout(
            subcommands=self.query_cfg_parents(
                "help_subcommands",
                default=HELP_SUBCOMMANDS_ALL,
                include_self=True,
            ),
            hide_parent=self.query_cfg_parents(
                "help_hide_parent",
                default=True,
                include_self=True,
            ),
            command_groups=tuple(
                self.query_cfg_inst("command_groups", default=()) or ()
            ),
        )
        renderer = HelpRenderer(self)
        fmt = self.get_help_formatter_class()
        if isinstance(fmt, type) and issubclass(fmt, RichRecursiveHelpFormatter):
            renderer.colorizer = help_document_colorizer
        self.help_renderer = renderer
        if hasattr(self.parser, "clak_help_renderer"):
            self.parser.clak_help_renderer = renderer
        if hasattr(self.parser, "parse_intermixed"):
            self.parser.parse_intermixed = self.query_cfg_parents(
                "parse_intermixed", default=True, include_self=True
            )

    def __getitem__(self, key):
        return self.children[key]

    def get_fname(self, attr="key"):
        "Get full name of the parser, use key instead of name by default"
        return super().get_fname(attr=attr)

    @property
    def subparsers(self):
        """Lazily create and return the subparsers object."""
        if self._subparsers is None:
            level = len(self.get_hierarchy())
            self._subparsers = self.parser.add_subparsers(
                dest=f"__cli_cmd__{level}",
                help="Available commands",
                parser_class=ArgumentParserPlus,
            )
        return self._subparsers

    # Argument management
    # ========================

    def _skip_argument_names(self) -> set:
        """Names of class Argument attrs to omit (view mixins override)."""
        return set()

    def _prepare_argument(  # pylint: disable=unused-argument
        self, key: str, arg: Argument
    ) -> Argument:
        """Hook to adjust an argument before attach (view mixins override)."""
        return arg

    def add_arguments(self, arguments: dict = None):
        """Initialize all argument options defined for this parser.

        This method:
        1. Collects arguments from meta__arguments_dict
        2. Collects arguments defined as class attributes
        3. Adds internal arguments like __cli_self__
        4. Creates all argument parser entries
        5. Copies ancestor flags onto this parser when inherit is on
        """
        if arguments is None:
            arguments = getattr(self, "meta__arguments_dict", None)
        if arguments is None:
            arguments = {}
        if not isinstance(arguments, dict):
            raise TypeError(f"Got {type(arguments)} instead of dict")
        arguments = dict(arguments)

        skip = self._skip_argument_names()

        # Add arguments from class attributes including inherited ones
        for cls in self.__class__.__mro__:
            for name, value in vars(cls).items():
                if isinstance(value, Argument) and name not in arguments:
                    if name in skip:
                        continue
                    value.destination = name
                    arguments[name] = value

        # Add __cli_self__ argument
        arguments["__cli_self__"] = Argument(help=argparse.SUPPRESS, default=self)

        ancestor_dests, ancestor_strings = self._ancestor_flag_index()
        local_flags: dict[str, Argument] = {}

        for key, arg in arguments.items():
            arg = self._prepare_argument(key, arg)
            overrides = None
            flag_strings = arg.flag_strings(key)
            if flag_strings:
                local_flags[key] = arg
                if key in ancestor_dests or (ancestor_strings & set(flag_strings)):
                    overrides = arg.suppress_attach_overrides()
            self.add_argument(key, arg, attach_overrides=overrides)

        self.local_flag_arguments = local_flags

        if self.query_cfg_parents("propagate_options", default=True, include_self=True):
            self._attach_inherited_parent_flags(arguments, local_flags)

    def _ancestor_flag_index(self) -> tuple[set[str], set[str]]:
        """Dests and option strings from ancestors' locally defined flags."""
        dests: set[str] = set()
        strings: set[str] = set()
        node = self.parent
        while node:
            for dest, arg in node.local_flag_arguments.items():
                dests.add(dest)
                strings.update(arg.flag_strings(dest))
            node = node.parent
        return dests, strings

    def _attach_inherited_parent_flags(
        self, arguments: dict, local_flags: dict[str, Argument]
    ) -> None:
        """Copy closest ancestor flags onto this parser (default=SUPPRESS)."""
        seen_dests = set(arguments)
        seen_strings: set[str] = set()
        for dest, arg in local_flags.items():
            seen_strings.update(arg.flag_strings(dest))

        group = self.query_cfg_parents(
            "propagate_options_group",
            default=PROPAGATE_OPTIONS_GROUP_DEFAULT,
            include_self=True,
        )
        group_extra = {
            "option_group": group,
            "argument_group": None,
        }

        node = self.parent
        while node:
            for dest, arg in node.local_flag_arguments.items():
                if dest in seen_dests:
                    continue
                if not arg.propagates():
                    continue
                strings = arg.flag_strings(dest)
                if not strings or (seen_strings & set(strings)):
                    continue
                self.add_argument(
                    dest,
                    arg,
                    attach_overrides=arg.suppress_attach_overrides(extra=group_extra),
                )
                seen_dests.add(dest)
                seen_strings.update(strings)
            node = node.parent

    def add_argument(
        self,
        key: str,
        arg: Optional[Argument] = None,
        attach_overrides: Optional[dict] = None,
        **kwargs: Any,
    ) -> None:
        """Add an argument to this parser.

        Args:
            key (str): The key/name for the argument
            arg (Argument): The argument object to add
            attach_overrides (dict): Kwargs applied only to this attach
            **kwargs (Any): Additional keyword arguments to pass to add_argument()

        This method adds a new argument to the parser. The argument can be either a
        positional argument or an optional flag, determined by the Argument object.
        """

        if arg is None:
            arg = Argument(**kwargs)

        arg.attach_arg_to_parser(key, self, attach_overrides=attach_overrides)

    # Subcommand management
    # ========================

    def add_subcommands(self, subcommands: dict = None):
        """Initialize all subcommands defined for this parser.

        This method:
        1. Collects subcommands from children dictionary
        2. Collects Command instances defined as class attributes
        3. Creates parser entries for all subcommands
        """

        if subcommands is None:
            subcommands = getattr(self, "meta__subcommands_dict", None)
        if subcommands is None:
            subcommands = {}
        if not isinstance(subcommands, dict):
            raise TypeError(f"Got {type(subcommands)} instead of dict")
        subcommands = dict(subcommands)

        # Collect Command instances from class attributes (child wins)
        for cls in self.__class__.__mro__:
            for attr_name, attr_value in cls.__dict__.items():
                if isinstance(attr_value, Command) and attr_name not in subcommands:
                    attr_value.destination = attr_name
                    subcommands[attr_name] = attr_value

        for key, arg in subcommands.items():
            self.add_subcommand(key, arg)

    def add_subcommand(self, key: str, arg=None, **kwargs) -> None:
        "Add a subcommand to this parser"
        if arg is None:
            arg = Command(**kwargs)

        arg.attach_sub_to_parser(key, self)

    # Help methods
    # ========================

    def show_help(self):
        """Display the help message for this parser."""
        self.parser.print_help()

    def show_usage(self):
        """Display the usage message for this parser."""
        self.parser.print_usage()

    def show_epilog(self):
        """Display the epilog message for this parser."""
        self.parser.print_epilog()

    # Execution helpers
    # ========================

    def parse_args(self, *args, **kwargs):
        """Parse argv; see ``Dispatcher.parse_args``."""
        return self.dispatcher.parse_args(*args, **kwargs)

    def dispatch(self, *args, **kwargs):
        """Parse and run; see ``Dispatcher.dispatch``."""
        return self.dispatcher.dispatch(*args, **kwargs)

    def cli_execute(self, *args, **kwargs):
        """Walk hooks and ``cli_run``; see ``Dispatcher.cli_execute``."""
        return self.dispatcher.cli_execute(*args, **kwargs)

    def clean_terminate(self, *args, **kwargs):
        """Handle a dispatch error; see ``Terminator.clean_terminate``."""
        return self.terminator.clean_terminate(*args, **kwargs)

    @property
    def ctx(self):
        """Last ``ClakContext`` from dispatch, or None before execute."""
        return self.dispatcher.ctx

    def cli_exit(self, status=0, message=None):
        """Exit the CLI application with given status and message.

        Args:
            status (int): Exit status code
            message (str): Optional message to display
        """
        self.parser.exit(status=status, message=message)

    def cli_exit_error(self, message):
        """Exit the CLI application with an error message.

        Args:
            message (str): Error message to display
        """
        self.parser.error(message)

    def cli_run(self, **kwargs: Any) -> None:  # pylint: disable=unused-argument
        """Execute the command implementation.

        This method should be overridden by subclasses to implement command behavior.
        The base implementation shows help for non-leaf nodes.

        Args:
            **kwargs: Additional keyword arguments from command line

        Raises:
            ClakNotImplementedError: If leaf node has no implementation
        """

        ctx = kwargs["ctx"]

        # Check if class is a leaf or not
        if len(ctx.cli_children) > 0:
            self.show_help()
        else:
            raise exception.ClakNotImplementedError(
                f"No 'cli_run' method found for {self}"
            )

    def _collect_cli_hooks(self) -> dict:
        """Bound ``cli_hook__*`` methods on this instance (once at build)."""
        hooks = {}
        for name in dir(self):
            if not name.startswith(CLI_HOOK_PREFIX):
                continue
            fn = getattr(self, name, None)
            if callable(fn):
                hooks[name] = fn
        return hooks

    def cli_group(self, ctx: ClakContext, **_: Any) -> None:
        """Execute group-level command behavior.

        Args:
            ctx: Command context object
            **_: Unused keyword arguments
        """

ctx property

Last ClakContext from dispatch, or None before execute.

subparsers property

Lazily create and return the subparsers object.

__init__(add_help=True, parent=None, name=None, key=None, parser=None, proc_name=None)

Initialize the parser.

Parameters:

Name Type Description Default
add_help bool

Whether to add help flags

True
parent ParserNode

Parent parser instance

None
name str

ParserNode name

None
key str

ParserNode key

None
parser ArgumentParser

Existing parser to use

None
proc_name str

Process name

None
Source code in clak/core/parser.py
def __init__(  # pylint: disable=too-many-arguments,too-many-positional-arguments
    self,
    add_help: bool = True,
    parent: "ParserNode" = None,
    name: str = None,
    key: str = None,
    parser: argparse.ArgumentParser = None,
    proc_name: str = None,
):
    """Initialize the parser.

    Args:
        add_help (bool): Whether to add help flags
        parent (ParserNode): Parent parser instance
        name (str): ParserNode name
        key (str): ParserNode key
        parser (ArgumentParser): Existing parser to use
        proc_name (str): Process name
    """
    self.logger = logger

    if parent is None:
        apply_debug_logging()

    super().__init__(parent=parent)

    self.dispatcher = Dispatcher(self)
    self.terminator = Terminator(self)
    self.help_args = []
    self.command_group = None
    self.command_help = None

    self.name = self.query_cfg_parents("name", default=self.__class__.__name__)
    self.key = key
    self.fkey = self.get_fname(attr="key")
    self.proc_name = proc_name
    self.add_help = add_help

    # Add children link
    self.children = {}
    self.registry = {}
    if parent:
        parent.children[self.key] = self
        self.registry = parent.registry
    self.registry[self.fkey] = self

    # Create or reuse parent parser
    if parser is None:
        self.parser = self.create_parser()
        self.proc_name = self.parser.prog
    else:
        self.parser = parser
        self.proc_name = self.parent.proc_name
        # add_parser() builds ArgumentParserPlus without clak_instance.
        if hasattr(self.parser, "clak_instance"):
            self.parser.clak_instance = self

    # Init _subparsers
    self._subparsers = None
    self.local_flag_arguments: dict[str, Argument] = {}

    self.add_arguments()
    self.add_subcommands()
    self._cli_hooks = self._collect_cli_hooks()
    self._install_help()

add_argument(key, arg=None, attach_overrides=None, **kwargs)

Add an argument to this parser.

Parameters:

Name Type Description Default
key str

The key/name for the argument

required
arg Argument

The argument object to add

None
attach_overrides dict

Kwargs applied only to this attach

None
**kwargs Any

Additional keyword arguments to pass to add_argument()

{}

This method adds a new argument to the parser. The argument can be either a positional argument or an optional flag, determined by the Argument object.

Source code in clak/core/parser.py
def add_argument(
    self,
    key: str,
    arg: Optional[Argument] = None,
    attach_overrides: Optional[dict] = None,
    **kwargs: Any,
) -> None:
    """Add an argument to this parser.

    Args:
        key (str): The key/name for the argument
        arg (Argument): The argument object to add
        attach_overrides (dict): Kwargs applied only to this attach
        **kwargs (Any): Additional keyword arguments to pass to add_argument()

    This method adds a new argument to the parser. The argument can be either a
    positional argument or an optional flag, determined by the Argument object.
    """

    if arg is None:
        arg = Argument(**kwargs)

    arg.attach_arg_to_parser(key, self, attach_overrides=attach_overrides)

add_arguments(arguments=None)

Initialize all argument options defined for this parser.

This method: 1. Collects arguments from meta__arguments_dict 2. Collects arguments defined as class attributes 3. Adds internal arguments like cli_self 4. Creates all argument parser entries 5. Copies ancestor flags onto this parser when inherit is on

Source code in clak/core/parser.py
def add_arguments(self, arguments: dict = None):
    """Initialize all argument options defined for this parser.

    This method:
    1. Collects arguments from meta__arguments_dict
    2. Collects arguments defined as class attributes
    3. Adds internal arguments like __cli_self__
    4. Creates all argument parser entries
    5. Copies ancestor flags onto this parser when inherit is on
    """
    if arguments is None:
        arguments = getattr(self, "meta__arguments_dict", None)
    if arguments is None:
        arguments = {}
    if not isinstance(arguments, dict):
        raise TypeError(f"Got {type(arguments)} instead of dict")
    arguments = dict(arguments)

    skip = self._skip_argument_names()

    # Add arguments from class attributes including inherited ones
    for cls in self.__class__.__mro__:
        for name, value in vars(cls).items():
            if isinstance(value, Argument) and name not in arguments:
                if name in skip:
                    continue
                value.destination = name
                arguments[name] = value

    # Add __cli_self__ argument
    arguments["__cli_self__"] = Argument(help=argparse.SUPPRESS, default=self)

    ancestor_dests, ancestor_strings = self._ancestor_flag_index()
    local_flags: dict[str, Argument] = {}

    for key, arg in arguments.items():
        arg = self._prepare_argument(key, arg)
        overrides = None
        flag_strings = arg.flag_strings(key)
        if flag_strings:
            local_flags[key] = arg
            if key in ancestor_dests or (ancestor_strings & set(flag_strings)):
                overrides = arg.suppress_attach_overrides()
        self.add_argument(key, arg, attach_overrides=overrides)

    self.local_flag_arguments = local_flags

    if self.query_cfg_parents("propagate_options", default=True, include_self=True):
        self._attach_inherited_parent_flags(arguments, local_flags)

add_subcommand(key, arg=None, **kwargs)

Add a subcommand to this parser

Source code in clak/core/parser.py
def add_subcommand(self, key: str, arg=None, **kwargs) -> None:
    "Add a subcommand to this parser"
    if arg is None:
        arg = Command(**kwargs)

    arg.attach_sub_to_parser(key, self)

add_subcommands(subcommands=None)

Initialize all subcommands defined for this parser.

This method: 1. Collects subcommands from children dictionary 2. Collects Command instances defined as class attributes 3. Creates parser entries for all subcommands

Source code in clak/core/parser.py
def add_subcommands(self, subcommands: dict = None):
    """Initialize all subcommands defined for this parser.

    This method:
    1. Collects subcommands from children dictionary
    2. Collects Command instances defined as class attributes
    3. Creates parser entries for all subcommands
    """

    if subcommands is None:
        subcommands = getattr(self, "meta__subcommands_dict", None)
    if subcommands is None:
        subcommands = {}
    if not isinstance(subcommands, dict):
        raise TypeError(f"Got {type(subcommands)} instead of dict")
    subcommands = dict(subcommands)

    # Collect Command instances from class attributes (child wins)
    for cls in self.__class__.__mro__:
        for attr_name, attr_value in cls.__dict__.items():
            if isinstance(attr_value, Command) and attr_name not in subcommands:
                attr_value.destination = attr_name
                subcommands[attr_name] = attr_value

    for key, arg in subcommands.items():
        self.add_subcommand(key, arg)

clean_terminate(*args, **kwargs)

Handle a dispatch error; see Terminator.clean_terminate.

Source code in clak/core/parser.py
def clean_terminate(self, *args, **kwargs):
    """Handle a dispatch error; see ``Terminator.clean_terminate``."""
    return self.terminator.clean_terminate(*args, **kwargs)

cli_execute(*args, **kwargs)

Walk hooks and cli_run; see Dispatcher.cli_execute.

Source code in clak/core/parser.py
def cli_execute(self, *args, **kwargs):
    """Walk hooks and ``cli_run``; see ``Dispatcher.cli_execute``."""
    return self.dispatcher.cli_execute(*args, **kwargs)

cli_exit(status=0, message=None)

Exit the CLI application with given status and message.

Parameters:

Name Type Description Default
status int

Exit status code

0
message str

Optional message to display

None
Source code in clak/core/parser.py
def cli_exit(self, status=0, message=None):
    """Exit the CLI application with given status and message.

    Args:
        status (int): Exit status code
        message (str): Optional message to display
    """
    self.parser.exit(status=status, message=message)

cli_exit_error(message)

Exit the CLI application with an error message.

Parameters:

Name Type Description Default
message str

Error message to display

required
Source code in clak/core/parser.py
def cli_exit_error(self, message):
    """Exit the CLI application with an error message.

    Args:
        message (str): Error message to display
    """
    self.parser.error(message)

cli_group(ctx, **_)

Execute group-level command behavior.

Parameters:

Name Type Description Default
ctx ClakContext

Command context object

required
**_ Any

Unused keyword arguments

{}
Source code in clak/core/parser.py
def cli_group(self, ctx: ClakContext, **_: Any) -> None:
    """Execute group-level command behavior.

    Args:
        ctx: Command context object
        **_: Unused keyword arguments
    """

cli_run(**kwargs)

Execute the command implementation.

This method should be overridden by subclasses to implement command behavior. The base implementation shows help for non-leaf nodes.

Parameters:

Name Type Description Default
**kwargs Any

Additional keyword arguments from command line

{}

Raises:

Type Description
ClakNotImplementedError

If leaf node has no implementation

Source code in clak/core/parser.py
def cli_run(self, **kwargs: Any) -> None:  # pylint: disable=unused-argument
    """Execute the command implementation.

    This method should be overridden by subclasses to implement command behavior.
    The base implementation shows help for non-leaf nodes.

    Args:
        **kwargs: Additional keyword arguments from command line

    Raises:
        ClakNotImplementedError: If leaf node has no implementation
    """

    ctx = kwargs["ctx"]

    # Check if class is a leaf or not
    if len(ctx.cli_children) > 0:
        self.show_help()
    else:
        raise exception.ClakNotImplementedError(
            f"No 'cli_run' method found for {self}"
        )

create_parser()

Create a new parser

Source code in clak/core/parser.py
def create_parser(self):
    "Create a new parser"
    usage = self.query_cfg_parents("help_usage", default=None)
    desc = self.query_cfg_parents("help_description", default=self.__doc__)
    epilog = self.query_cfg_parents("help_epilog", default=None)

    fenv = FormatEnv({"self": self})
    usage = prepare_docstring(usage, variables=fenv.get())
    desc = prepare_docstring(desc, variables=fenv.get())
    epilog = prepare_docstring(epilog, variables=fenv.get())
    parser = ArgumentParserPlus(
        prog=self.proc_name,
        usage=usage,
        description=desc,
        epilog=epilog,
        formatter_class=self.get_help_formatter_class(),
        add_help=self.add_help,
        exit_on_error=False,
        clak_instance=self,
        parse_intermixed=self.query_cfg_parents(
            "parse_intermixed", default=True, include_self=True
        ),
    )
    return parser

dispatch(*args, **kwargs)

Parse and run; see Dispatcher.dispatch.

Source code in clak/core/parser.py
def dispatch(self, *args, **kwargs):
    """Parse and run; see ``Dispatcher.dispatch``."""
    return self.dispatcher.dispatch(*args, **kwargs)

get_fname(attr='key')

Get full name of the parser, use key instead of name by default

Source code in clak/core/parser.py
def get_fname(self, attr="key"):
    "Get full name of the parser, use key instead of name by default"
    return super().get_fname(attr=attr)

get_help_formatter_class()

Return the argparse HelpFormatter class for this node.

Mixins set meta__help_formatter (or Meta.help_formatter). Unset walks parents, then RichRecursiveHelpFormatter. Opt out with Meta.help_formatter = RecursiveHelpFormatter.

Source code in clak/core/parser.py
def get_help_formatter_class(self):
    """Return the argparse HelpFormatter class for this node.

    Mixins set ``meta__help_formatter`` (or ``Meta.help_formatter``).
    Unset walks parents, then ``RichRecursiveHelpFormatter``.
    Opt out with ``Meta.help_formatter = RecursiveHelpFormatter``.
    """
    from clak.comp.help import (  # pylint: disable=import-outside-toplevel
        RichRecursiveHelpFormatter,
    )

    return self.query_cfg_parents(
        "help_formatter",
        default=RichRecursiveHelpFormatter,
        include_self=True,
    )

parse_args(*args, **kwargs)

Parse argv; see Dispatcher.parse_args.

Source code in clak/core/parser.py
def parse_args(self, *args, **kwargs):
    """Parse argv; see ``Dispatcher.parse_args``."""
    return self.dispatcher.parse_args(*args, **kwargs)

show_epilog()

Display the epilog message for this parser.

Source code in clak/core/parser.py
def show_epilog(self):
    """Display the epilog message for this parser."""
    self.parser.print_epilog()

show_help()

Display the help message for this parser.

Source code in clak/core/parser.py
def show_help(self):
    """Display the help message for this parser."""
    self.parser.print_help()

show_usage()

Display the usage message for this parser.

Source code in clak/core/parser.py
def show_usage(self):
    """Display the usage message for this parser."""
    self.parser.print_usage()

PprintViewMixin

Bases: TextLayoutOptMixin

Auto-render command results with :class:~clak.views.PprintView.

Adds --line-length. Configure exposed flags with Meta.view_cli_options.

Source code in clak/comp/views.py
class PprintViewMixin(TextLayoutOptMixin):
    """Auto-render command results with :class:`~clak.views.PprintView`.

    Adds ``--line-length``. Configure exposed flags with ``Meta.view_cli_options``.
    """

    _view_cli_option_names = _LAYER_TEXT_LAYOUT_DESTS
    meta__cli_view = PprintView

RawViewMixin

Bases: TextLayoutOptMixin

Auto-render command results with :class:~clak.views.RawView.

Adds --line-length. Configure exposed flags with Meta.view_cli_options.

Source code in clak/comp/views.py
class RawViewMixin(TextLayoutOptMixin):
    """Auto-render command results with :class:`~clak.views.RawView`.

    Adds ``--line-length``. Configure exposed flags with ``Meta.view_cli_options``.
    """

    _view_cli_option_names = _LAYER_TEXT_LAYOUT_DESTS
    meta__cli_view = RawView

RecursiveHelpFormatter

Bases: RawDescriptionHelpFormatter

Public opt-out of colored --help (plain layout).

Layout is produced by HelpRenderer. This class remains an argparse HelpFormatter so Meta.help_formatter and formatter_class identity stay valid.

Source code in clak/core/help_render.py
class RecursiveHelpFormatter(RawDescriptionHelpFormatter):
    """Public opt-out of colored --help (plain layout).

    Layout is produced by HelpRenderer. This class remains an argparse
    HelpFormatter so Meta.help_formatter and formatter_class identity stay
    valid.
    """

    config__max_help_position = MAX_HELP_POSITION

    def __init__(self, *args, max_help_position=None, **kwargs):
        super().__init__(
            *args, max_help_position=self.config__max_help_position, **kwargs
        )

RichHelpMixin

Same default as Parser (Rich help formatter).

Optional. Useful to re-opt-in a child after a parent sets Meta.help_formatter = RecursiveHelpFormatter.

Source code in clak/comp/help.py
class RichHelpMixin:  # pylint: disable=too-few-public-methods
    """Same default as ``Parser`` (Rich help formatter).

    Optional. Useful to re-opt-in a child after a parent sets
    ``Meta.help_formatter = RecursiveHelpFormatter``.
    """

    meta__help_formatter = RichRecursiveHelpFormatter

RstViewMixin

Bases: TextViewOptMixin

Auto-render command results with :class:~clak.views.RstView.

Adds --format (view / raw) and --line-length. Configure exposed flags with Meta.view_cli_options.

Source code in clak/comp/views.py
class RstViewMixin(TextViewOptMixin):
    """Auto-render command results with :class:`~clak.views.RstView`.

    Adds ``--format`` (``view`` / ``raw``) and ``--line-length``.
    Configure exposed flags with ``Meta.view_cli_options``.
    """

    _view_cli_option_names = _LAYER_TEXT_LAYOUT_DESTS | _LAYER_TEXT_DESTS
    meta__cli_view = RstView

ShowViewMixin

Bases: TableViewOptMixin

Auto-render command results with :class:~clak.views.ShowView.

Adds --columns, --add-index / --no-add-index, --format, --sort-columns, --sort-mode, --width, and --wrap. Configure exposed flags with Meta.view_cli_options.

Source code in clak/comp/views.py
class ShowViewMixin(TableViewOptMixin):
    """Auto-render command results with :class:`~clak.views.ShowView`.

    Adds ``--columns``, ``--add-index`` / ``--no-add-index``,
    ``--format``, ``--sort-columns``, ``--sort-mode``, ``--width``,
    and ``--wrap``.
    Configure exposed flags with ``Meta.view_cli_options``.
    """

    _view_cli_option_names = _LAYER_TABLE_DESTS
    meta__cli_view = ShowView

SubParser

Bases: ArgParseItem

Represents a subcommand parser that can be added to a parent parser.

This class handles creation of nested command structures, allowing for hierarchical command-line interfaces via argparse add_subparsers.

Most keyword arguments are passed through to :meth:argparse._SubParsersAction.add_parser. Clak-only kwargs (stripped before argparse):

  • command_group: Optional key for a subcommand help section. Pair with Meta.command_groups on the parent Parser. Formatter metadata only; not a second add_subparsers. Commands with no key stay under leftover subcommands:.

Attributes:

Name Type Description
meta__help_flags bool

Whether to enable -h and --help support

meta__usage str

Custom usage message

meta__description str

Custom description message

meta__epilog str

Custom epilog message

Source code in clak/core/descriptors.py
class SubParser(ArgParseItem):
    """Represents a subcommand parser that can be added to a parent parser.

    This class handles creation of nested command structures, allowing for hierarchical
    command-line interfaces via argparse ``add_subparsers``.

    Most keyword arguments are passed through to
    :meth:`argparse._SubParsersAction.add_parser`. Clak-only kwargs (stripped
    before argparse):

    - ``command_group``: Optional key for a subcommand help section. Pair with
      ``Meta.command_groups`` on the parent Parser. Formatter metadata only;
      not a second ``add_subparsers``. Commands with no key stay under
      leftover ``subcommands:``.

    Attributes:
        meta__help_flags (bool): Whether to enable -h and --help support
        meta__usage (str): Custom usage message
        meta__description (str): Custom description message
        meta__epilog (str): Custom epilog message
    """

    # If true, enable -h and --help support
    meta__help_flags = True

    meta__usage = None
    meta__description = None
    meta__epilog = None

    def __init__(self, cls, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.cls = cls

    def attach_sub_to_parser(self, key: str, config: "ParserNode") -> "ParserNode":
        """Create a subcommand parser for this command.

        Creates a new subparser for the command and configures it with the appropriate
        help text and options. Validates that the command name is valid.

        Args:
            key (str): Name of the subcommand
            config (ParserNode): Parent parser configuration object

        Raises:
            ValueError: If command name contains spaces

        Returns:
            ParserNode: The created child parser instance
        """

        if " " in key:
            raise ValueError(
                f"Command name '{key}' contains spaces. Command names must not contain spaces."
            )

        logger.debug(
            "Create new subparser %s.%s",
            config.get_fname(attr="key"),
            key,
        )

        # Fetch help from class
        parser_help = self.kwargs.get(
            "help",
            self.cls.query_cfg_inst(
                self.cls, "help_description", default=self.cls.__doc__
            ),
        )
        parser_help_enabled = self.kwargs.get(
            "help_flags",
            self.cls.query_cfg_inst(self.cls, "help_flags", default=True),
        )

        ctx_vars = {"key": key, "self": config}

        # Create a new subparser for this command (flat structure)
        parser_help = prepare_docstring(first_doc_line(parser_help), variables=ctx_vars)
        parser_kwargs = dict(self.kwargs)
        parser_kwargs.update(
            {
                "formatter_class": config.get_help_formatter_class(),
                "add_help": parser_help_enabled,  # Add support for --help
                "exit_on_error": False,
                "help": parser_help,
            }
        )
        command_group = parser_kwargs.pop("command_group", None)

        # Create parser
        subparser = config.subparsers.add_parser(
            key,
            **parser_kwargs,
        )

        # Create an instance of the command class with the subparser
        child = self.cls(parent=config, parser=subparser, key=key)
        child.command_group = command_group
        child.command_help = parser_help
        ctx_vars["self"] = child

        child_usage = child.query_cfg_inst("help_usage", default=None)
        child_desc = first_doc_line(
            child.query_cfg_inst("help_description", default=child.__doc__)
        )
        child_epilog = child.query_cfg_inst("help_epilog", default=None)

        # Reconfigure subparser
        child_usage = prepare_docstring(child_usage, variables=ctx_vars)
        child_desc = prepare_docstring(child_desc, variables=ctx_vars)
        child_epilog = prepare_docstring(child_epilog, variables=ctx_vars)

        subparser.add_help = False
        subparser.usage = child_usage
        subparser.description = child_desc
        subparser.epilog = child_epilog
        subparser.formatter_class = child.get_help_formatter_class()

        return child

attach_sub_to_parser(key, config)

Create a subcommand parser for this command.

Creates a new subparser for the command and configures it with the appropriate help text and options. Validates that the command name is valid.

Parameters:

Name Type Description Default
key str

Name of the subcommand

required
config ParserNode

Parent parser configuration object

required

Raises:

Type Description
ValueError

If command name contains spaces

Returns:

Name Type Description
ParserNode ParserNode

The created child parser instance

Source code in clak/core/descriptors.py
def attach_sub_to_parser(self, key: str, config: "ParserNode") -> "ParserNode":
    """Create a subcommand parser for this command.

    Creates a new subparser for the command and configures it with the appropriate
    help text and options. Validates that the command name is valid.

    Args:
        key (str): Name of the subcommand
        config (ParserNode): Parent parser configuration object

    Raises:
        ValueError: If command name contains spaces

    Returns:
        ParserNode: The created child parser instance
    """

    if " " in key:
        raise ValueError(
            f"Command name '{key}' contains spaces. Command names must not contain spaces."
        )

    logger.debug(
        "Create new subparser %s.%s",
        config.get_fname(attr="key"),
        key,
    )

    # Fetch help from class
    parser_help = self.kwargs.get(
        "help",
        self.cls.query_cfg_inst(
            self.cls, "help_description", default=self.cls.__doc__
        ),
    )
    parser_help_enabled = self.kwargs.get(
        "help_flags",
        self.cls.query_cfg_inst(self.cls, "help_flags", default=True),
    )

    ctx_vars = {"key": key, "self": config}

    # Create a new subparser for this command (flat structure)
    parser_help = prepare_docstring(first_doc_line(parser_help), variables=ctx_vars)
    parser_kwargs = dict(self.kwargs)
    parser_kwargs.update(
        {
            "formatter_class": config.get_help_formatter_class(),
            "add_help": parser_help_enabled,  # Add support for --help
            "exit_on_error": False,
            "help": parser_help,
        }
    )
    command_group = parser_kwargs.pop("command_group", None)

    # Create parser
    subparser = config.subparsers.add_parser(
        key,
        **parser_kwargs,
    )

    # Create an instance of the command class with the subparser
    child = self.cls(parent=config, parser=subparser, key=key)
    child.command_group = command_group
    child.command_help = parser_help
    ctx_vars["self"] = child

    child_usage = child.query_cfg_inst("help_usage", default=None)
    child_desc = first_doc_line(
        child.query_cfg_inst("help_description", default=child.__doc__)
    )
    child_epilog = child.query_cfg_inst("help_epilog", default=None)

    # Reconfigure subparser
    child_usage = prepare_docstring(child_usage, variables=ctx_vars)
    child_desc = prepare_docstring(child_desc, variables=ctx_vars)
    child_epilog = prepare_docstring(child_epilog, variables=ctx_vars)

    subparser.add_help = False
    subparser.usage = child_usage
    subparser.description = child_desc
    subparser.epilog = child_epilog
    subparser.formatter_class = child.get_help_formatter_class()

    return child

XDGConfigMixin

XDG path flags and config-file loading.

Adds: - --conf-file: $XDG_CONFIG_HOME/<app>/config.yaml - --data-dir: $XDG_DATA_HOME/<app> (hidden) - --cache-dir: $XDG_CACHE_HOME/<app> (hidden) - --log-dir: $XDG_CACHE_HOME/<app>/logs (hidden)

<app> comes from Meta.app_name, else the parser name / class name. Defaults respect $XDG_CONFIG_HOME, $XDG_DATA_HOME, and $XDG_CACHE_HOME when set.

On dispatch, cli_hook__config loads --conf-file (JSON always; YAML with the config extra). Missing file yields {} unless Meta.config_required is true. Loaded data is available as ctx.config (dict) and cli_root.config (attribute namespace).

Source code in clak/comp/config.py
class XDGConfigMixin:  # pylint: disable=too-few-public-methods
    """XDG path flags and config-file loading.

    Adds:
    - ``--conf-file``: ``$XDG_CONFIG_HOME/<app>/config.yaml``
    - ``--data-dir``: ``$XDG_DATA_HOME/<app>`` (hidden)
    - ``--cache-dir``: ``$XDG_CACHE_HOME/<app>`` (hidden)
    - ``--log-dir``: ``$XDG_CACHE_HOME/<app>/logs`` (hidden)

    ``<app>`` comes from ``Meta.app_name``, else the parser name / class name.
    Defaults respect ``$XDG_CONFIG_HOME``, ``$XDG_DATA_HOME``, and
    ``$XDG_CACHE_HOME`` when set.

    On dispatch, ``cli_hook__config`` loads ``--conf-file`` (JSON always;
    YAML with the ``config`` extra). Missing file yields ``{}`` unless
    ``Meta.config_required`` is true. Loaded data is available as
    ``ctx.config`` (dict) and ``cli_root.config`` (attribute namespace).
    """

    xdg_config = Argument(
        "--conf-file",
        help="Configuration file to use",
    )
    xdg_data_dir = Argument(
        "--data-dir",
        help=argparse.SUPPRESS,
    )
    xdg_cache_dir = Argument(
        "--cache-dir",
        help=argparse.SUPPRESS,
    )
    xdg_log_dir = Argument(
        "--log-dir",
        help=argparse.SUPPRESS,
    )

    meta__config__config_required = MetaSetting(
        help="If true, missing --conf-file raises ClakUserError",
    )

    _XDG_ARG_DEFAULTS = (
        ("xdg_config", "conf_file"),
        ("xdg_data_dir", "data_dir"),
        ("xdg_cache_dir", "cache_dir"),
        ("xdg_log_dir", "log_dir"),
    )

    def _xdg_app_name(self) -> str:
        """Resolve the application name used in XDG paths."""
        name = self.query_cfg_parents("app_name", default=None)
        if not name:
            name = getattr(self, "name", None) or self.__class__.__name__
        return sanitize_xdg_app_name(name)

    def add_arguments(self, arguments: dict = None):
        """Apply XDG defaults from app name / env, then register arguments."""
        if arguments is None:
            arguments = getattr(self, "meta__arguments_dict", None)
        arguments = dict(arguments or {})
        paths = resolve_xdg_paths(self._xdg_app_name())

        for attr_name, path_key in self._XDG_ARG_DEFAULTS:
            template = getattr(type(self), attr_name, None)
            if not isinstance(template, Argument):
                continue
            if attr_name in arguments:
                continue
            kwargs = dict(template.kwargs)
            kwargs.setdefault("default", paths[path_key])
            arg = Argument(*template.args, **kwargs)
            arg.destination = attr_name
            arguments[attr_name] = arg

        return super().add_arguments(arguments)

    def cli_hook__config(self, instance, ctx, **_):
        """Load ``--conf-file`` once and expose it on ctx / root."""
        if ctx.cli_first:
            path = getattr(ctx.args, "xdg_config", None)
            required = bool(
                self.query_cfg_parents(
                    "config_required", default=False, include_self=True
                )
            )

            if not path:
                data: dict[str, Any] = {}
                if required:
                    raise ClakUserError(
                        "Configuration file path is required",
                        advice="Pass --conf-file PATH",
                    )
            else:
                conf_path = Path(path)
                if not conf_path.is_file():
                    if required:
                        raise ClakUserError(
                            f"Configuration file not found: {conf_path}",
                            advice="Create the file or pass --conf-file PATH",
                        )
                    logger.debug(
                        "Config file missing, using empty config: %s", conf_path
                    )
                    data = {}
                else:
                    data = load_config_file(conf_path)

            ctx.plugins["config"] = data
            ctx.plugins["config_path"] = str(path) if path else None
            ctx.cli_root.config = ObjectNamespace(**data)
            logger.debug(
                "Config loaded for %s from %s (%d keys)",
                instance,
                path,
                len(data),
            )

        # Re-attach each hierarchy step (fresh ObjectNamespace per node)
        ctx.config = ctx.plugins.get("config", {})

add_arguments(arguments=None)

Apply XDG defaults from app name / env, then register arguments.

Source code in clak/comp/config.py
def add_arguments(self, arguments: dict = None):
    """Apply XDG defaults from app name / env, then register arguments."""
    if arguments is None:
        arguments = getattr(self, "meta__arguments_dict", None)
    arguments = dict(arguments or {})
    paths = resolve_xdg_paths(self._xdg_app_name())

    for attr_name, path_key in self._XDG_ARG_DEFAULTS:
        template = getattr(type(self), attr_name, None)
        if not isinstance(template, Argument):
            continue
        if attr_name in arguments:
            continue
        kwargs = dict(template.kwargs)
        kwargs.setdefault("default", paths[path_key])
        arg = Argument(*template.args, **kwargs)
        arg.destination = attr_name
        arguments[attr_name] = arg

    return super().add_arguments(arguments)

cli_hook__config(instance, ctx, **_)

Load --conf-file once and expose it on ctx / root.

Source code in clak/comp/config.py
def cli_hook__config(self, instance, ctx, **_):
    """Load ``--conf-file`` once and expose it on ctx / root."""
    if ctx.cli_first:
        path = getattr(ctx.args, "xdg_config", None)
        required = bool(
            self.query_cfg_parents(
                "config_required", default=False, include_self=True
            )
        )

        if not path:
            data: dict[str, Any] = {}
            if required:
                raise ClakUserError(
                    "Configuration file path is required",
                    advice="Pass --conf-file PATH",
                )
        else:
            conf_path = Path(path)
            if not conf_path.is_file():
                if required:
                    raise ClakUserError(
                        f"Configuration file not found: {conf_path}",
                        advice="Create the file or pass --conf-file PATH",
                    )
                logger.debug(
                    "Config file missing, using empty config: %s", conf_path
                )
                data = {}
            else:
                data = load_config_file(conf_path)

        ctx.plugins["config"] = data
        ctx.plugins["config_path"] = str(path) if path else None
        ctx.cli_root.config = ObjectNamespace(**data)
        logger.debug(
            "Config loaded for %s from %s (%d keys)",
            instance,
            path,
            len(data),
        )

    # Re-attach each hierarchy step (fresh ObjectNamespace per node)
    ctx.config = ctx.plugins.get("config", {})