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.
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.
    """

    def attach_arg_to_parser(self, key: str, config: "ParserNode") -> argparse.Action:
        """Create and add an argument to the parser.

        Args:
            key (str): The argument key/name
            config (ParserNode): The parser configuration object

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

        argument_group_title = kwargs.pop("argument_group", None)
        option_group_title = kwargs.pop("option_group", None)
        exclusive_key = kwargs.pop("exclusive_group", None)

        if argument_group_title is not None and option_group_title is not None:
            raise ValueError(
                f"Argument {key!r} cannot set both argument_group and "
                f"option_group (got {argument_group_title!r} and "
                f"{option_group_title!r})"
            )
        help_group_title = (
            argument_group_title
            if argument_group_title is not None
            else option_group_title
        )

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

        target = parser
        if help_group_title is not None:
            groups = getattr(parser, "_clak_argument_groups", None)
            if groups is None:
                groups = {}
                setattr(parser, "_clak_argument_groups", groups)
            if help_group_title not in groups:
                groups[help_group_title] = parser.add_argument_group(help_group_title)
            target = groups[help_group_title]

        if exclusive_key is not None:
            exclusive_groups = getattr(parser, "_clak_exclusive_groups", None)
            if exclusive_groups is None:
                exclusive_groups = {}
                setattr(parser, "_clak_exclusive_groups", exclusive_groups)
            # Nest under the help section when present; key by (parent id, name)
            # so the same exclusive name can exist under different help titles.
            exclusive_cache_key = (id(target), exclusive_key)
            if exclusive_cache_key not in exclusive_groups:
                exclusive_groups[exclusive_cache_key] = (
                    target.add_mutually_exclusive_group()
                )
            target = exclusive_groups[exclusive_cache_key]

        target.add_argument(*args, **kwargs)

        return parser

attach_arg_to_parser(key, config)

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

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") -> argparse.Action:
    """Create and add an argument to the parser.

    Args:
        key (str): The argument key/name
        config (ParserNode): The parser configuration object

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

    argument_group_title = kwargs.pop("argument_group", None)
    option_group_title = kwargs.pop("option_group", None)
    exclusive_key = kwargs.pop("exclusive_group", None)

    if argument_group_title is not None and option_group_title is not None:
        raise ValueError(
            f"Argument {key!r} cannot set both argument_group and "
            f"option_group (got {argument_group_title!r} and "
            f"{option_group_title!r})"
        )
    help_group_title = (
        argument_group_title
        if argument_group_title is not None
        else option_group_title
    )

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

    target = parser
    if help_group_title is not None:
        groups = getattr(parser, "_clak_argument_groups", None)
        if groups is None:
            groups = {}
            setattr(parser, "_clak_argument_groups", groups)
        if help_group_title not in groups:
            groups[help_group_title] = parser.add_argument_group(help_group_title)
        target = groups[help_group_title]

    if exclusive_key is not None:
        exclusive_groups = getattr(parser, "_clak_exclusive_groups", None)
        if exclusive_groups is None:
            exclusive_groups = {}
            setattr(parser, "_clak_exclusive_groups", exclusive_groups)
        # Nest under the help section when present; key by (parent id, name)
        # so the same exclusive name can exist under different help titles.
        exclusive_cache_key = (id(target), exclusive_key)
        if exclusive_cache_key not in exclusive_groups:
            exclusive_groups[exclusive_cache_key] = (
                target.add_mutually_exclusive_group()
            )
        target = exclusive_groups[exclusive_cache_key]

    target.add_argument(*args, **kwargs)

    return parser

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",
        # dest="use_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,
        # help="external argcomplete script for auto completion of the executable"
    )
    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 execution 4. Implementing cli_group() for command group behavior

Attributes:

Name Type Description
arguments_dict dict

Dictionary of argument name to ArgParseItem

children dict

Dictionary of subcommand name to subcommand class

meta__name str

ParserNode name

Source code in clak/core/parser.py
 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
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
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 execution
    4. Implementing cli_group() for command group behavior

    Attributes:
        arguments_dict (dict): Dictionary of argument name to ArgParseItem
        children (dict): Dictionary of subcommand name to subcommand class
        meta__name (str): ParserNode name
    """

    arguments_dict: dict[str, ArgParseItem] = {}
    children: dict[str, type] = {}  # Dictionary of subcommand name to subcommand class

    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__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,
        inject_as_subparser: bool = True,
        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
            inject_as_subparser (bool): Ignored. Kept so existing callers
                do not TypeError. Only ``USE_SUBPARSERS`` in
                ``clak.core.descriptors`` controls argparse inject.
            proc_name (str): Process name
        """
        del inject_as_subparser

        self.logger = logger

        super().__init__(parent=parent)

        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

        # Init _subparsers
        self._subparsers = None

        # Add arguments and subcommands
        # meta__arguments_dict = {}
        # meta__subcommands_dict = {}
        self.add_arguments()
        self.add_subcommands()

    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,
        )
        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 __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,
            )
            command_groups = self.query_cfg_inst("command_groups", default=())
            help_subcommands = self.query_cfg_parents(
                "help_subcommands",
                default=HELP_SUBCOMMANDS_ALL,
                include_self=True,
            )
            help_hide_parent = self.query_cfg_parents(
                "help_hide_parent",
                default=True,
                include_self=True,
            )
            # pylint: disable=protected-access
            self._subparsers._clak_help = HelpLayout(
                subcommands=help_subcommands,
                hide_parent=help_hide_parent,
                command_groups=tuple(command_groups),
            )
        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 arguments_dict
        2. Collects arguments defined as class attributes
        3. Adds internal arguments like __cli_self__
        4. Creates all argument parser entries
        """
        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)

        # Create all options
        for key, arg in arguments.items():
            arg = self._prepare_argument(key, arg)
            self.add_argument(key, arg)

    def add_argument(
        self, key: str, arg: Optional[Argument] = 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
            **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)

    # 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():
            # arg.attach_sub_to_parser(key, self)
            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 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 cli_group(self, ctx: SimpleNamespace, **_: Any) -> None:
        """Execute group-level command behavior.

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

    @staticmethod
    def _exception_exit_code(err, default=1):
        rc = getattr(err, "rc", default)
        return rc if isinstance(rc, int) else default

    @staticmethod
    def _exception_advice(err):
        advice = getattr(err, "advice", None)
        if isinstance(advice, str):
            logger.warning(advice)

    def _terminate_app_exception(self, err):
        """Default handler for app exceptions (Paasify-style: rc + message)."""
        self._exception_advice(err)
        print(err, file=sys.stderr)
        rc = self._exception_exit_code(err)
        logger.critical(
            "Program exited with: error %s: %s",
            rc,
            err.__class__.__name__,
        )
        sys.exit(rc)

    @staticmethod
    def _iter_exception_entries(entries):
        for entry in entries or []:
            if isinstance(entry, (tuple, list)) and entry:
                exc_type = entry[0]
                handler = entry[1] if len(entry) > 1 else None
                yield exc_type, handler
            else:
                yield entry, None

    def _run_exception_handler(self, handler, err):
        if handler is None:
            self._terminate_app_exception(err)
            return
        result = handler(self, err)
        if isinstance(result, int):
            sys.exit(result)
        sys.exit(self._exception_exit_code(err))

    def clean_terminate(self, err, known_exceptions=None):
        """Handle program termination based on exception type.

        Processing order (Paasify-style chain):

        1. ``Meta.known_exceptions`` on the root parser (class or ``(class, handler)``)
        2. ``Meta.exception_handlers`` (third-party libs: YAML, shell, …)
        3. Built-in Clak exceptions
        4. Broken pipe (``| head`` / ``| tail``) - quiet exit
        5. Common OS errors

        If nothing matches, return and let ``dispatch()`` report an unexpected bug.

        Args:
            err (Exception): The exception that triggered termination
            known_exceptions (list): List of exception types to handle specially
        """

        # 1. App-known exceptions (e.g. PaasifyError hierarchy)
        for exc_type, handler in self._iter_exception_entries(known_exceptions):
            if isinstance(err, exc_type):
                self._run_exception_handler(handler, err)

        # 2. Registered third-party / library handlers
        extra_handlers = self.query_cfg_parents("exception_handlers", default=[])
        for exc_type, handler in self._iter_exception_entries(extra_handlers):
            if isinstance(err, exc_type):
                self._run_exception_handler(handler, err)

        # 3. Clak parse errors — show usage first (leaf parser when known)
        if isinstance(err, exception.ClakParseError):
            if err.parser is not None:
                err.parser.print_usage()
            else:
                self.show_usage()
            print(f"{err}", file=sys.stderr)
            sys.exit(err.rc)

        # 4. User-facing Clak errors
        if isinstance(err, exception.ClakUserError):
            self._exception_advice(err)
            print(f"{err}", file=sys.stderr)
            sys.exit(err.rc)

        # 5. Other Clak errors (app / bug)
        if isinstance(err, exception.ClakError):
            err_name = err.__class__.__name__
            self._exception_advice(err)
            err_message = err.message or err.__doc__
            print(f"{err}", file=sys.stderr)
            logger.critical(
                "Program exited with bug %s(%s): %s",
                err_name,
                err.rc,
                err_message,
            )
            sys.exit(err.rc)

        # 6. Broken pipe from | head / | tail - quiet exit (no bug log)
        if isinstance(err, BrokenPipeError):
            _exit_broken_pipe()

        # 7. OS errors (BrokenPipeError already handled above)
        if isinstance(err, OSError):
            logger.critical("Program exited with OS error: %s", err)
            sys.exit(err.errno if err.errno is not None else 1)

    def parse_args(
        self, args: Optional[Union[str, List[str], Dict[str, Any]]] = None
    ) -> argparse.Namespace:
        """Parse command line arguments.

        Args:
            args: Arguments to parse, can be:
                - None: Use sys.argv[1:]
                - str: Shell-style split via ``shlex.split``
                - list: Use directly
                - dict: Return as-is

        Returns:
            Namespace: Parsed argument namespace

        Raises:
            ValueError: If args is invalid type
        """
        parser = self.parser
        # argcomplete.autocomplete(parser)

        # args = args[0] if len(args) > 0 else sys.argv[1:]

        if args is None:
            args = sys.argv[1:]
        elif isinstance(args, str):
            args = shlex.split(args)
        elif isinstance(args, list):
            pass
        elif isinstance(args, dict):
            return args
        else:
            raise ValueError(f"Invalid args type: {type(args)}")

        return parser.parse_args(args)

    def dispatch(  # pylint: disable=too-many-branches
        self,
        args: Optional[Union[str, List[str], Dict[str, Any]]] = None,
        trace: bool = False,
        **_: Any,
    ) -> Any:
        """Main dispatch function for command execution.

        Args:
            args: Arguments to parse
            **_: Unused keyword arguments
        """

        # Process or reuse args
        # if args is None:
        error = None
        try:
            args = self.parse_args(args)
            args = args.__dict__
        except argparse.ArgumentError as err:
            error = exception.ClakParseError(
                format_argument_error(err),
                parser=getattr(err, "clak_parser", None),
            )
            # raise exception.ClakParseError(msg) from err

        if not error:
            if not isinstance(args, dict):
                raise TypeError(
                    f"Parsed args must be a dict, got {type(args).__name__}"
                )

            # Check for trace mode
            if "app_trace_mode" in args:
                trace = args["app_trace_mode"]
            if CLAK_DEBUG:
                trace = True

            # Leaf command (may carry Meta.cli_view / view mixins on nested cmds)
            cli_leaf = args.get("__cli_self__", self)

            # Run app command + view render (pipe breaks during print hit clean_terminate)
            try:
                data = self.cli_execute(args=args)

                # Prepare viewer output (CLI view mixins may stash settings on root)
                view_settings = getattr(self, "_clak_view_settings", None) or {}
                if isinstance(data, ClakView):
                    render_kwargs = merge_view_settings(
                        getattr(data, "settings", None), view_settings
                    )
                    data.render(**render_kwargs)
                else:
                    viewer = cli_leaf.query_cfg_parents("cli_view", default=None)
                    if isinstance(viewer, type) and issubclass(viewer, ClakView):
                        viewer = viewer()
                    if viewer is not None:
                        if not isinstance(viewer, ClakView):
                            raise TypeError(
                                "Meta.cli_view must be a ClakView instance or subclass"
                            )
                        viewer.render(data, **view_settings)

                return data

            except Exception as err:  # pylint: disable=broad-exception-caught
                error = err

        if trace is True:
            # print("TRACE")
            # Show traceback if debug mode is enabled
            logger.error("".join(traceback.format_exception(error)))
            # print("TRACE")

        # Process exception handling
        known_exceptions = self.query_cfg_parents("known_exceptions", default=[])
        self.clean_terminate(error, known_exceptions)

        # Developer catchall — unexpected bug (Paasify-style)
        if trace is False:
            logger.error("".join(traceback.format_exception(error)))
        logger.critical(
            "Uncaught error %s; this may be a bug! Please report to the developer.",
            error.__class__.__name__,
        )
        logger.critical("Error: %s", error)
        sys.exit(1)

    def cli_execute(  # pylint: disable=too-many-locals,too-many-statements
        self, args: Optional[Dict[str, Any]] = None
    ) -> Any:
        """Execute the command with given arguments.

        Args:
            args: Arguments to parse

        Raises:
            ClakParseError: If argument parsing fails
            NotImplementedError: If command has no implementation
        """
        if not isinstance(args, dict):
            raise TypeError(
                f"cli_execute args must be a dict, got {type(args).__name__}"
            )

        # Prepare args and context
        hook_list = {}

        # args = args.__dict__
        cli_command_hier = [
            value
            for key, value in sorted(args.items())
            if key.startswith("__cli_cmd__")
        ]
        args = {
            key: value
            for key, value in args.items()
            if not key.startswith("__cli_cmd__")
        }

        cli_self = self
        if "__cli_self__" in args:
            cli_self = args.pop("__cli_self__")

        # Prepare data
        fn_group_name = "cli_group"
        fn_exec_name = "cli_run"
        fn_hook_prefix = "cli_hook__"
        name = self.name
        hierarchy = cli_self.get_hierarchy()
        node_count = len(hierarchy)

        logger.debug("Run instance %s", cli_self)

        ctx = {}
        ctx["registry"] = self.registry

        # Fetch settings
        ctx["name"] = name
        ctx["app_name"] = self.query_cfg_parents("app_name", default=name)
        ctx["app_proc_name"] = self.query_cfg_parents(
            "app_proc_name", default=self.proc_name
        )
        # ctx["app_env_prefix"] = self.query_cfg_parents(
        #     "app_env_prefix", default=name.upper()
        # )

        # Loop constant
        ctx["cli_self"] = cli_self
        ctx["cli_root"] = self
        ctx["cli_depth"] = node_count
        ctx["cli_commands"] = cli_command_hier
        ctx["args"] = ObjectNamespace(**args)

        # Shared data
        ctx["data"] = {}
        ctx["plugins"] = {}

        narrow_width = self.query_cfg_parents("runtime_narrow_width", default=None)
        ctx["runtime"] = detect_runtime(narrow_width=narrow_width)
        ctx["facts"] = detect_facts()

        # Loop var init
        ctx["cli_first"] = True
        ctx["cli_state"] = None
        ctx["cli_methods"] = None

        # Execute all nodes in hierarchy
        ret = None
        # pylint: disable=attribute-defined-outside-init
        for idx, node in enumerate(hierarchy):
            last_node = idx == (node_count - 1)

            logger.info("Processing node %d:%s.%s", idx, node, fn_group_name)
            # print(f"Node {idx}:{node}")

            # Prepare hooks list (per hierarchy node — mixins on subcommands)
            cls_hooks = [
                method for method in dir(node) if method.startswith(fn_hook_prefix)
            ]
            for hook_name in cls_hooks:
                hook_fn = getattr(node, hook_name, None)
                if hook_fn is not None:
                    # Last node with this name wins (leaf mixin rebinds root)
                    hook_list[hook_name] = hook_fn

            # Update ctx with node attributes
            ctx["cli_parent"] = hierarchy[-2] if len(hierarchy) > 1 else None
            ctx["cli_parents"] = hierarchy[:idx]
            ctx["cli_children"] = dict(node.children)
            ctx["cli_last"] = last_node
            ctx["cli_hooks"] = hook_list
            ctx["cli_index"] = idx

            # Sort ctx dict by keys before creating namespace
            sorted_ctx = dict(sorted(ctx.items()))
            _ctx = ObjectNamespace(**sorted_ctx)
            _ctx.cli_state = "run_hooks"

            # Process hooks
            for name, hook_fn in hook_list.items():
                # hook_fn = getattr(self, hook, None)
                # if hook_fn is not None:
                logger.info("Run hook %d:%s.%s", idx, node, name)
                hook_fn(node, _ctx)

            # Store the list of available plugins methods
            _ctx.cli_methods = getattr(node, "cli_methods", {})

            # Run group_run
            _ctx.cli_state = "run_groups"

            group_fn = getattr(node, fn_group_name, None)
            # print ("GROUP FN", group_fn)
            if group_fn is not None:
                logger.info(
                    "Group function execute: %d:%s.%s", idx, node, fn_group_name
                )
                group_fn(ctx=_ctx, **_ctx.__dict__)

            # Run leaf only if last node
            _ctx.cli_state = "run_exec"
            if last_node is True:
                run_fn = getattr(node, fn_exec_name, None)

                logger.info("Run function execute: %d:%s.%s", idx, node, fn_exec_name)
                ret = run_fn(ctx=_ctx, **_ctx.args.__dict__)

            # Change status
            ctx["cli_first"] = False

        return ret

subparsers property

Lazily create and return the subparsers object.

__init__(add_help=True, parent=None, name=None, key=None, parser=None, inject_as_subparser=True, 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
inject_as_subparser bool

Ignored. Kept so existing callers do not TypeError. Only USE_SUBPARSERS in clak.core.descriptors controls argparse inject.

True
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,
    inject_as_subparser: bool = True,
    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
        inject_as_subparser (bool): Ignored. Kept so existing callers
            do not TypeError. Only ``USE_SUBPARSERS`` in
            ``clak.core.descriptors`` controls argparse inject.
        proc_name (str): Process name
    """
    del inject_as_subparser

    self.logger = logger

    super().__init__(parent=parent)

    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

    # Init _subparsers
    self._subparsers = None

    # Add arguments and subcommands
    # meta__arguments_dict = {}
    # meta__subcommands_dict = {}
    self.add_arguments()
    self.add_subcommands()

add_argument(key, arg=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
**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, **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
        **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)

add_arguments(arguments=None)

Initialize all argument options defined for this parser.

This method: 1. Collects arguments from arguments_dict 2. Collects arguments defined as class attributes 3. Adds internal arguments like cli_self 4. Creates all argument parser entries

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 arguments_dict
    2. Collects arguments defined as class attributes
    3. Adds internal arguments like __cli_self__
    4. Creates all argument parser entries
    """
    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)

    # Create all options
    for key, arg in arguments.items():
        arg = self._prepare_argument(key, arg)
        self.add_argument(key, arg)

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():
        # arg.attach_sub_to_parser(key, self)
        self.add_subcommand(key, arg)

clean_terminate(err, known_exceptions=None)

Handle program termination based on exception type.

Processing order (Paasify-style chain):

  1. Meta.known_exceptions on the root parser (class or (class, handler))
  2. Meta.exception_handlers (third-party libs: YAML, shell, …)
  3. Built-in Clak exceptions
  4. Broken pipe (| head / | tail) - quiet exit
  5. Common OS errors

If nothing matches, return and let dispatch() report an unexpected bug.

Parameters:

Name Type Description Default
err Exception

The exception that triggered termination

required
known_exceptions list

List of exception types to handle specially

None
Source code in clak/core/parser.py
def clean_terminate(self, err, known_exceptions=None):
    """Handle program termination based on exception type.

    Processing order (Paasify-style chain):

    1. ``Meta.known_exceptions`` on the root parser (class or ``(class, handler)``)
    2. ``Meta.exception_handlers`` (third-party libs: YAML, shell, …)
    3. Built-in Clak exceptions
    4. Broken pipe (``| head`` / ``| tail``) - quiet exit
    5. Common OS errors

    If nothing matches, return and let ``dispatch()`` report an unexpected bug.

    Args:
        err (Exception): The exception that triggered termination
        known_exceptions (list): List of exception types to handle specially
    """

    # 1. App-known exceptions (e.g. PaasifyError hierarchy)
    for exc_type, handler in self._iter_exception_entries(known_exceptions):
        if isinstance(err, exc_type):
            self._run_exception_handler(handler, err)

    # 2. Registered third-party / library handlers
    extra_handlers = self.query_cfg_parents("exception_handlers", default=[])
    for exc_type, handler in self._iter_exception_entries(extra_handlers):
        if isinstance(err, exc_type):
            self._run_exception_handler(handler, err)

    # 3. Clak parse errors — show usage first (leaf parser when known)
    if isinstance(err, exception.ClakParseError):
        if err.parser is not None:
            err.parser.print_usage()
        else:
            self.show_usage()
        print(f"{err}", file=sys.stderr)
        sys.exit(err.rc)

    # 4. User-facing Clak errors
    if isinstance(err, exception.ClakUserError):
        self._exception_advice(err)
        print(f"{err}", file=sys.stderr)
        sys.exit(err.rc)

    # 5. Other Clak errors (app / bug)
    if isinstance(err, exception.ClakError):
        err_name = err.__class__.__name__
        self._exception_advice(err)
        err_message = err.message or err.__doc__
        print(f"{err}", file=sys.stderr)
        logger.critical(
            "Program exited with bug %s(%s): %s",
            err_name,
            err.rc,
            err_message,
        )
        sys.exit(err.rc)

    # 6. Broken pipe from | head / | tail - quiet exit (no bug log)
    if isinstance(err, BrokenPipeError):
        _exit_broken_pipe()

    # 7. OS errors (BrokenPipeError already handled above)
    if isinstance(err, OSError):
        logger.critical("Program exited with OS error: %s", err)
        sys.exit(err.errno if err.errno is not None else 1)

cli_execute(args=None)

Execute the command with given arguments.

Parameters:

Name Type Description Default
args Optional[Dict[str, Any]]

Arguments to parse

None

Raises:

Type Description
ClakParseError

If argument parsing fails

NotImplementedError

If command has no implementation

Source code in clak/core/parser.py
def cli_execute(  # pylint: disable=too-many-locals,too-many-statements
    self, args: Optional[Dict[str, Any]] = None
) -> Any:
    """Execute the command with given arguments.

    Args:
        args: Arguments to parse

    Raises:
        ClakParseError: If argument parsing fails
        NotImplementedError: If command has no implementation
    """
    if not isinstance(args, dict):
        raise TypeError(
            f"cli_execute args must be a dict, got {type(args).__name__}"
        )

    # Prepare args and context
    hook_list = {}

    # args = args.__dict__
    cli_command_hier = [
        value
        for key, value in sorted(args.items())
        if key.startswith("__cli_cmd__")
    ]
    args = {
        key: value
        for key, value in args.items()
        if not key.startswith("__cli_cmd__")
    }

    cli_self = self
    if "__cli_self__" in args:
        cli_self = args.pop("__cli_self__")

    # Prepare data
    fn_group_name = "cli_group"
    fn_exec_name = "cli_run"
    fn_hook_prefix = "cli_hook__"
    name = self.name
    hierarchy = cli_self.get_hierarchy()
    node_count = len(hierarchy)

    logger.debug("Run instance %s", cli_self)

    ctx = {}
    ctx["registry"] = self.registry

    # Fetch settings
    ctx["name"] = name
    ctx["app_name"] = self.query_cfg_parents("app_name", default=name)
    ctx["app_proc_name"] = self.query_cfg_parents(
        "app_proc_name", default=self.proc_name
    )
    # ctx["app_env_prefix"] = self.query_cfg_parents(
    #     "app_env_prefix", default=name.upper()
    # )

    # Loop constant
    ctx["cli_self"] = cli_self
    ctx["cli_root"] = self
    ctx["cli_depth"] = node_count
    ctx["cli_commands"] = cli_command_hier
    ctx["args"] = ObjectNamespace(**args)

    # Shared data
    ctx["data"] = {}
    ctx["plugins"] = {}

    narrow_width = self.query_cfg_parents("runtime_narrow_width", default=None)
    ctx["runtime"] = detect_runtime(narrow_width=narrow_width)
    ctx["facts"] = detect_facts()

    # Loop var init
    ctx["cli_first"] = True
    ctx["cli_state"] = None
    ctx["cli_methods"] = None

    # Execute all nodes in hierarchy
    ret = None
    # pylint: disable=attribute-defined-outside-init
    for idx, node in enumerate(hierarchy):
        last_node = idx == (node_count - 1)

        logger.info("Processing node %d:%s.%s", idx, node, fn_group_name)
        # print(f"Node {idx}:{node}")

        # Prepare hooks list (per hierarchy node — mixins on subcommands)
        cls_hooks = [
            method for method in dir(node) if method.startswith(fn_hook_prefix)
        ]
        for hook_name in cls_hooks:
            hook_fn = getattr(node, hook_name, None)
            if hook_fn is not None:
                # Last node with this name wins (leaf mixin rebinds root)
                hook_list[hook_name] = hook_fn

        # Update ctx with node attributes
        ctx["cli_parent"] = hierarchy[-2] if len(hierarchy) > 1 else None
        ctx["cli_parents"] = hierarchy[:idx]
        ctx["cli_children"] = dict(node.children)
        ctx["cli_last"] = last_node
        ctx["cli_hooks"] = hook_list
        ctx["cli_index"] = idx

        # Sort ctx dict by keys before creating namespace
        sorted_ctx = dict(sorted(ctx.items()))
        _ctx = ObjectNamespace(**sorted_ctx)
        _ctx.cli_state = "run_hooks"

        # Process hooks
        for name, hook_fn in hook_list.items():
            # hook_fn = getattr(self, hook, None)
            # if hook_fn is not None:
            logger.info("Run hook %d:%s.%s", idx, node, name)
            hook_fn(node, _ctx)

        # Store the list of available plugins methods
        _ctx.cli_methods = getattr(node, "cli_methods", {})

        # Run group_run
        _ctx.cli_state = "run_groups"

        group_fn = getattr(node, fn_group_name, None)
        # print ("GROUP FN", group_fn)
        if group_fn is not None:
            logger.info(
                "Group function execute: %d:%s.%s", idx, node, fn_group_name
            )
            group_fn(ctx=_ctx, **_ctx.__dict__)

        # Run leaf only if last node
        _ctx.cli_state = "run_exec"
        if last_node is True:
            run_fn = getattr(node, fn_exec_name, None)

            logger.info("Run function execute: %d:%s.%s", idx, node, fn_exec_name)
            ret = run_fn(ctx=_ctx, **_ctx.args.__dict__)

        # Change status
        ctx["cli_first"] = False

    return ret

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 SimpleNamespace

Command context object

required
**_ Any

Unused keyword arguments

{}
Source code in clak/core/parser.py
def cli_group(self, ctx: SimpleNamespace, **_: 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,
    )
    return parser

dispatch(args=None, trace=False, **_)

Main dispatch function for command execution.

Parameters:

Name Type Description Default
args Optional[Union[str, List[str], Dict[str, Any]]]

Arguments to parse

None
**_ Any

Unused keyword arguments

{}
Source code in clak/core/parser.py
def dispatch(  # pylint: disable=too-many-branches
    self,
    args: Optional[Union[str, List[str], Dict[str, Any]]] = None,
    trace: bool = False,
    **_: Any,
) -> Any:
    """Main dispatch function for command execution.

    Args:
        args: Arguments to parse
        **_: Unused keyword arguments
    """

    # Process or reuse args
    # if args is None:
    error = None
    try:
        args = self.parse_args(args)
        args = args.__dict__
    except argparse.ArgumentError as err:
        error = exception.ClakParseError(
            format_argument_error(err),
            parser=getattr(err, "clak_parser", None),
        )
        # raise exception.ClakParseError(msg) from err

    if not error:
        if not isinstance(args, dict):
            raise TypeError(
                f"Parsed args must be a dict, got {type(args).__name__}"
            )

        # Check for trace mode
        if "app_trace_mode" in args:
            trace = args["app_trace_mode"]
        if CLAK_DEBUG:
            trace = True

        # Leaf command (may carry Meta.cli_view / view mixins on nested cmds)
        cli_leaf = args.get("__cli_self__", self)

        # Run app command + view render (pipe breaks during print hit clean_terminate)
        try:
            data = self.cli_execute(args=args)

            # Prepare viewer output (CLI view mixins may stash settings on root)
            view_settings = getattr(self, "_clak_view_settings", None) or {}
            if isinstance(data, ClakView):
                render_kwargs = merge_view_settings(
                    getattr(data, "settings", None), view_settings
                )
                data.render(**render_kwargs)
            else:
                viewer = cli_leaf.query_cfg_parents("cli_view", default=None)
                if isinstance(viewer, type) and issubclass(viewer, ClakView):
                    viewer = viewer()
                if viewer is not None:
                    if not isinstance(viewer, ClakView):
                        raise TypeError(
                            "Meta.cli_view must be a ClakView instance or subclass"
                        )
                    viewer.render(data, **view_settings)

            return data

        except Exception as err:  # pylint: disable=broad-exception-caught
            error = err

    if trace is True:
        # print("TRACE")
        # Show traceback if debug mode is enabled
        logger.error("".join(traceback.format_exception(error)))
        # print("TRACE")

    # Process exception handling
    known_exceptions = self.query_cfg_parents("known_exceptions", default=[])
    self.clean_terminate(error, known_exceptions)

    # Developer catchall — unexpected bug (Paasify-style)
    if trace is False:
        logger.error("".join(traceback.format_exception(error)))
    logger.critical(
        "Uncaught error %s; this may be a bug! Please report to the developer.",
        error.__class__.__name__,
    )
    logger.critical("Error: %s", error)
    sys.exit(1)

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=None)

Parse command line arguments.

Parameters:

Name Type Description Default
args Optional[Union[str, List[str], Dict[str, Any]]]

Arguments to parse, can be: - None: Use sys.argv[1:] - str: Shell-style split via shlex.split - list: Use directly - dict: Return as-is

None

Returns:

Name Type Description
Namespace Namespace

Parsed argument namespace

Raises:

Type Description
ValueError

If args is invalid type

Source code in clak/core/parser.py
def parse_args(
    self, args: Optional[Union[str, List[str], Dict[str, Any]]] = None
) -> argparse.Namespace:
    """Parse command line arguments.

    Args:
        args: Arguments to parse, can be:
            - None: Use sys.argv[1:]
            - str: Shell-style split via ``shlex.split``
            - list: Use directly
            - dict: Return as-is

    Returns:
        Namespace: Parsed argument namespace

    Raises:
        ValueError: If args is invalid type
    """
    parser = self.parser
    # argcomplete.autocomplete(parser)

    # args = args[0] if len(args) > 0 else sys.argv[1:]

    if args is None:
        args = sys.argv[1:]
    elif isinstance(args, str):
        args = shlex.split(args)
    elif isinstance(args, list):
        pass
    elif isinstance(args, dict):
        return args
    else:
        raise ValueError(f"Invalid args type: {type(args)}")

    return parser.parse_args(args)

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

A recursive help formatter to help command discovery.

Source code in clak/core/argparse_.py
class RecursiveHelpFormatter(argparse.RawDescriptionHelpFormatter):
    """A recursive help formatter to help command discovery."""

    config__max_help_position = 30

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

    def _format_action_invocation(self, action):
        """Keep option help stable across Python versions.

        Python 3.13+ changed optional formatting from
        ``-s ARGS, --long ARGS`` to ``-s, --long ARGS``. Pin the older form so
        Clak help output does not drift with the interpreter.
        """
        if not action.option_strings:
            default = self._get_default_metavar_for_positional(action)
            (metavar,) = self._metavar_formatter(action, default)(1)
            return metavar

        parts = []
        if action.nargs == 0:
            parts.extend(action.option_strings)
        else:
            default = self._get_default_metavar_for_optional(action)
            args_string = self._format_args(action, default)
            for option_string in action.option_strings:
                parts.append(f"{option_string} {args_string}")
        return ", ".join(parts)

    def _get_default_metavar_for_positional(self, action):
        "Automatically show positional as uppercase"
        return action.dest.upper()

    # Show default values
    def _get_help_string(self, action):
        help_msg = action.help
        if help_msg is None:
            help_msg = ""

        if "%(default)" not in help_msg:
            if action.default is not SUPPRESS:
                defaulting_nargs = [OPTIONAL, ZERO_OR_MORE]
                if action.option_strings or action.nargs in defaulting_nargs:
                    help_msg += " (default: %(default)s)"
        return help_msg

    @staticmethod
    def _grouped_subcommand_sections(action, layout=None):
        """Yield (title, choice actions) for named, unknown-key, then leftover."""
        if layout is None:
            layout = help_layout_for(action)
        named_groups = layout.command_groups or ()
        named_titles = dict(named_groups)
        named_keys = [key for key, _title in named_groups]

        by_group = {}
        for subaction in action._choices_actions:
            group_key = getattr(subaction, "_clak_command_group", None)
            by_group.setdefault(group_key, []).append(subaction)

        for key in named_keys:
            members = by_group.get(key)
            if members:
                yield named_titles[key], members

        unknown_keys = []
        for subaction in action._choices_actions:
            group_key = getattr(subaction, "_clak_command_group", None)
            if group_key is None or group_key in named_titles:
                continue
            if group_key not in unknown_keys:
                unknown_keys.append(group_key)
        for key in unknown_keys:
            title = key if str(key).endswith(":") else f"{key}:"
            yield title, by_group.get(key)

        leftover = by_group.get(None)
        if leftover:
            yield "subcommands:", leftover

    @staticmethod
    def _nested_cmd_label(prefix, dest, hide_parent, level=0):
        if hide_parent:
            return f"{HELP_NESTED_INDENT * level}{dest}"
        return f"{prefix}{dest}"

    def _max_subcommand_label_width(self, action, list_nested, hide_parent):
        """Longest left label among listed subcommands (nested when enabled)."""
        widest = 0

        def walk(parser, prefix, level):
            nonlocal widest
            for act in parser._actions:
                if not isinstance(act, argparse._SubParsersAction):
                    continue
                for subaction in act._choices_actions:
                    if subaction.help != argparse.SUPPRESS:
                        widest = max(
                            widest,
                            len(
                                self._nested_cmd_label(
                                    prefix, subaction.dest, hide_parent, level
                                )
                            ),
                        )
                    if list_nested:
                        walk(
                            act.choices[subaction.dest],
                            f"{prefix}{subaction.dest} ",
                            level + 1,
                        )

        for subaction in action._choices_actions:
            if subaction.help != argparse.SUPPRESS:
                widest = max(widest, len(subaction.dest))
            if list_nested:
                walk(action.choices[subaction.dest], f"{subaction.dest} ", 1)
        return widest

    # Ensure all subparsers are shown
    def _format_action(self, action):  # pylint: disable=too-many-locals
        "Override and improve helper output"

        # Notes:
        # Subcommand sections are formatter metadata (command_group), not
        # argparse add_argument_group / a second add_subparsers.
        # - See: https://docs.python.org/3/library/argparse.html#argument-groups
        # Implement register for subcommands:
        # - See: https://docs.python.org/3/library/argparse.html#registering-custom-types-or-actions

        if not isinstance(action, argparse._SubParsersAction):
            out = super()._format_action(action)
            return out

        layout = help_layout_for(action)
        list_nested = layout.subcommands == HELP_SUBCOMMANDS_ALL
        hide_parent = layout.hide_parent

        # Get the original format parts
        parts = []
        bullet: str = "  "

        help_position = min(self._action_max_length + 2, self._max_help_position)
        action_width = help_position - self._current_indent - 2
        action_width = max(
            action_width,
            self._max_subcommand_label_width(action, list_nested, hide_parent) + 2,
        )
        max_action_width = self._max_help_position - self._current_indent - 2
        if max_action_width > 0:
            action_width = min(action_width, max_action_width)
        help_position = action_width + self._current_indent + 2
        help_width = max(self._width - help_position, 11)

        def format_cmd_line(cmd, help_msg, prefix=""):
            if not help_msg:
                return f"{prefix}{cmd}\n"
            if len(cmd) >= action_width:
                wrapped = textwrap.wrap(help_msg, help_width) or [""]
                lines = [f"{prefix}{cmd}"]
                lines.extend(f"{' ' * help_position}{chunk}" for chunk in wrapped)
                return "".join(f"{line}\n" for line in lines)
            return f"{prefix}{cmd:<{action_width}}{help_msg}\n"

        def add_subparser_to_parts(
            parser: argparse.ArgumentParser,
            prefix: str = "",
            level: int = 0,
            indent: str = "..",
        ):
            _indent = indent * level

            for act in parser._actions:
                if isinstance(act, argparse._SubParsersAction):
                    for subaction in act._choices_actions:
                        choice = act.choices[subaction.dest]
                        full_cmd = f"{prefix}{subaction.dest}"
                        if subaction.help != argparse.SUPPRESS:
                            help_msg = subaction.help or ""
                            parts.append(
                                format_cmd_line(
                                    self._nested_cmd_label(
                                        prefix,
                                        subaction.dest,
                                        hide_parent,
                                        level,
                                    ),
                                    help_msg,
                                    prefix=f"{_indent}{bullet}",
                                )
                            )

                        add_subparser_to_parts(
                            choice,
                            prefix=f"{full_cmd} ",
                            level=level + 1,
                            indent=indent,
                        )

        def append_choice(subaction):
            choice = action.choices[subaction.dest]
            if subaction.help != argparse.SUPPRESS:
                help_msg = subaction.help or ""
                parts.append(format_cmd_line(subaction.dest, help_msg, prefix=bullet))
            if list_nested:
                add_subparser_to_parts(
                    choice, prefix=f"{subaction.dest} ", level=1, indent=""
                )

        grouped = any(
            getattr(subaction, "_clak_command_group", None)
            for subaction in action._choices_actions
        )
        if not grouped:
            for subaction in action._choices_actions:
                append_choice(subaction)
            if len(parts) > 0:
                parts.insert(0, "\nsubcommands:\n")
            return "".join(parts)

        for title, subactions in self._grouped_subcommand_sections(action, layout):
            parts.append(f"\n{title}\n")
            for subaction in subactions:
                append_choice(subaction)

        return "".join(parts)

    def format_help(self):
        """Drop an empty ``positional arguments:`` heading.

        Subparsers live in that argparse group, but Clak prints them under
        ``subcommands:``. When there are no real positionals, the empty
        heading is noise.
        """
        text = super().format_help()
        heading = _("positional arguments")
        return re.sub(rf"(?m)^{re.escape(heading)}:\n+(?! )", "", text)

format_help()

Drop an empty positional arguments: heading.

Subparsers live in that argparse group, but Clak prints them under subcommands:. When there are no real positionals, the empty heading is noise.

Source code in clak/core/argparse_.py
def format_help(self):
    """Drop an empty ``positional arguments:`` heading.

    Subparsers live in that argparse group, but Clak prints them under
    ``subcommands:``. When there are no real positionals, the empty
    heading is noise.
    """
    text = super().format_help()
    heading = _("positional arguments")
    return re.sub(rf"(?m)^{re.escape(heading)}:\n+(?! )", "", text)

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. It supports both subparser and injection modes.

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. It supports both subparser and injection modes.

    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, use_subparsers: bool = USE_SUBPARSERS, **kwargs):
        super().__init__(*args, **kwargs)
        self.cls = cls
        self.use_subparsers = use_subparsers

    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."
            )

        if self.use_subparsers:

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

            # 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),
            )
            # parser_aliases = self.kwargs.get(
            #     "aliases",
            #     [],
            # )

            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,
                    # "aliases": parser_aliases,
                }
            )
            command_group = parser_kwargs.pop("command_group", None)
            # if parser_help is not None:
            #     parser_kwargs["help"] = parser_help

            # Create parser
            subparser = config.subparsers.add_parser(
                key,
                **parser_kwargs,
            )
            # pylint: disable=protected-access
            config.subparsers._choices_actions[-1]._clak_command_group = command_group

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

            # logger.debug(
            #     "Create new SUBPARSER %s %s %s",
            #     child.get_fname(attr="key"),
            #     key,
            #     self.kwargs,
            # )

            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)
            # print(f"DESC: |{desc}|")

            # 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  # child.query_cfg_inst("help_enable", default=True)
            )
            subparser.usage = child_usage
            subparser.description = child_desc
            subparser.epilog = child_epilog
            subparser.formatter_class = child.get_help_formatter_class()

            # pprint (subparser.__dict__)

        else:
            # This part is in BETA

            # Create nested structure
            child = self.cls(parent=config)
            # Pass help text from Command class kwargs
            child.parser.help = self.kwargs.get("help", child.__doc__)
            argparse_inject_as_subparser(config.parser, key, child.parser)

        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."
        )

    if self.use_subparsers:

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

        # 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),
        )
        # parser_aliases = self.kwargs.get(
        #     "aliases",
        #     [],
        # )

        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,
                # "aliases": parser_aliases,
            }
        )
        command_group = parser_kwargs.pop("command_group", None)
        # if parser_help is not None:
        #     parser_kwargs["help"] = parser_help

        # Create parser
        subparser = config.subparsers.add_parser(
            key,
            **parser_kwargs,
        )
        # pylint: disable=protected-access
        config.subparsers._choices_actions[-1]._clak_command_group = command_group

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

        # logger.debug(
        #     "Create new SUBPARSER %s %s %s",
        #     child.get_fname(attr="key"),
        #     key,
        #     self.kwargs,
        # )

        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)
        # print(f"DESC: |{desc}|")

        # 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  # child.query_cfg_inst("help_enable", default=True)
        )
        subparser.usage = child_usage
        subparser.description = child_desc
        subparser.epilog = child_epilog
        subparser.formatter_class = child.get_help_formatter_class()

        # pprint (subparser.__dict__)

    else:
        # This part is in BETA

        # Create nested structure
        child = self.cls(parent=config)
        # Pass help text from Command class kwargs
        child.parser.help = self.kwargs.get("help", child.__doc__)
        argparse_inject_as_subparser(config.parser, key, child.parser)

    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", {})