Skip to content

Module API - Parser

Alt Text

Prefer the top-level package for app code:

from clak import Parser, Argument, Command
# Optional: Arg (positionals), Opt (flags) - Argument still accepts both

Descriptors (Argument, Arg, Opt, SubParser, docstring helpers):

clak.core.descriptors

CLI descriptors: Argument, SubParser, MetaSetting, docstring helpers.

Extracted from parser.py to keep the build/execute core smaller. Public imports remain available from clak, clak.parser, and clak.core.descriptors.

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

ArgParseItem

Bases: Fn

Base class for argument parser items.

This class represents a generic argument parser item that can be added to an argument parser. It provides common functionality for handling destinations and building parameter dictionaries.

Attributes:

Name Type Description
_destination str

The destination name for the argument value

Source code in clak/core/descriptors.py
class ArgParseItem(Fn):
    """Base class for argument parser items.

    This class represents a generic argument parser item that can be added to an argument parser.
    It provides common functionality for handling destinations and building parameter dictionaries.

    Attributes:
        _destination (str): The destination name for the argument value
    """

    _destination: str = None

    @property
    def destination(self) -> Optional[str]:
        """Get the destination name for this argument.

        Returns:
            str: The destination name, derived from the argument name if not explicitly set
            None: If no destination can be determined
        """
        return self._get_best_dest()

    @destination.setter
    def destination(self, value):
        self._destination = value

    def _get_best_dest(self) -> str:
        "Get the best destination name for this argument"
        if self._destination is not None:
            return self._destination

        # If no arguments, return None
        if not self.args:
            return None

        # Get first argument which should be the flag name
        arg = self.args[0]

        # Remove leading dashes and convert remaining dashes to underscores
        if arg.startswith("--"):
            key = arg[2:].replace("-", "_")
        elif arg.startswith("-"):
            # For short flags like -v, use the longer version if available
            if len(self.args) > 1 and self.args[1].startswith("--"):
                key = self.args[1][2:].replace("-", "_")
            else:
                key = arg[1:]
        else:
            key = arg.replace("-", "_")

        return key

    def build_params(self, dest: str) -> Tuple[tuple, dict]:
        """Build parameter dictionary for argument parser.

        Args:
            dest (str): Destination name for the argument

        Returns:
            tuple: A tuple containing (args, kwargs) for argument parser

        Raises:
            ValueError: If no arguments are found
        """
        # Create parser arguments
        kwargs = self.kwargs

        # kind = "option"
        if len(self.args) > 0:
            if len(self.args) > 2:
                raise ValueError(
                    f"Too many arguments found for {self.__class__.__name__}: {self.args}"
                )

            args = self.args

            arg1 = args[0]
            if not arg1.startswith("-"):
                # Remove first position arg to avoid argparse error:
                # ValueError: dest supplied twice for positional argument
                kwargs["metavar"] = args[0]
                args = ()
                # kind = "argument"

        elif dest:
            if len(dest) <= 2:
                args = (f"-{dest}",)
            else:
                args = (f"--{dest}",)
        else:
            raise ValueError(
                f"No arguments found for {self.__class__.__name__}: {self.__dict__}"
            )

        # Update dest if forced
        if dest:
            kwargs["dest"] = dest

        # if kind == "argument":
        #     if "dest" in kwargs:
        #         if len(args) == 1:
        #             # Remove first position arg to avoid argparse error:
        #             # ValueError: dest supplied twice for positional argument
        #             kwargs["metavar"] = args[0]
        #             args = ()
        #         else:
        #             raise ValueError(
        #                 f"Too many arguments found for {self.__class__.__name__}: {self.__dict__}"
        #             )

        return args, kwargs

destination property writable

Get the destination name for this argument.

Returns:

Name Type Description
str Optional[str]

The destination name, derived from the argument name if not explicitly set

None Optional[str]

If no destination can be determined

build_params(dest)

Build parameter dictionary for argument parser.

Parameters:

Name Type Description Default
dest str

Destination name for the argument

required

Returns:

Name Type Description
tuple Tuple[tuple, dict]

A tuple containing (args, kwargs) for argument parser

Raises:

Type Description
ValueError

If no arguments are found

Source code in clak/core/descriptors.py
def build_params(self, dest: str) -> Tuple[tuple, dict]:
    """Build parameter dictionary for argument parser.

    Args:
        dest (str): Destination name for the argument

    Returns:
        tuple: A tuple containing (args, kwargs) for argument parser

    Raises:
        ValueError: If no arguments are found
    """
    # Create parser arguments
    kwargs = self.kwargs

    # kind = "option"
    if len(self.args) > 0:
        if len(self.args) > 2:
            raise ValueError(
                f"Too many arguments found for {self.__class__.__name__}: {self.args}"
            )

        args = self.args

        arg1 = args[0]
        if not arg1.startswith("-"):
            # Remove first position arg to avoid argparse error:
            # ValueError: dest supplied twice for positional argument
            kwargs["metavar"] = args[0]
            args = ()
            # kind = "argument"

    elif dest:
        if len(dest) <= 2:
            args = (f"-{dest}",)
        else:
            args = (f"--{dest}",)
    else:
        raise ValueError(
            f"No arguments found for {self.__class__.__name__}: {self.__dict__}"
        )

    # Update dest if forced
    if dest:
        kwargs["dest"] = dest

    # if kind == "argument":
    #     if "dest" in kwargs:
    #         if len(args) == 1:
    #             # Remove first position arg to avoid argparse error:
    #             # ValueError: dest supplied twice for positional argument
    #             kwargs["metavar"] = args[0]
    #             args = ()
    #         else:
    #             raise ValueError(
    #                 f"Too many arguments found for {self.__class__.__name__}: {self.__dict__}"
    #             )

    return args, kwargs

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

FormatEnv

Format env for docstring variable substitution

Source code in clak/core/descriptors.py
class FormatEnv:  # pylint: disable=too-few-public-methods
    "Format env for docstring variable substitution"

    _default = {
        "type": "type FUNC",
    }

    def __init__(self, variables=None):
        self._variables = dict(variables or {})

    def get(self):
        "Get dict of vars"
        out = {}
        out.update(self._default)
        for key, value in self._variables.items():
            # Normalize object.__doc__ across Python versions (3.13+ cleandoc).
            if key == "self" and value is not None:
                out[key] = CleandocProxy(value)
            else:
                out[key] = value
        return out

get()

Get dict of vars

Source code in clak/core/descriptors.py
def get(self):
    "Get dict of vars"
    out = {}
    out.update(self._default)
    for key, value in self._variables.items():
        # Normalize object.__doc__ across Python versions (3.13+ cleandoc).
        if key == "self" and value is not None:
            out[key] = CleandocProxy(value)
        else:
            out[key] = value
    return out

MetaSetting

Bases: Fn

A setting that is used to configure a node

Source code in clak/core/descriptors.py
class MetaSetting(Fn):  # pylint: disable=too-few-public-methods
    "A setting that is used to configure a node"

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

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

first_doc_line(text)

Get the first non-empty line from a text string.

Parameters:

Name Type Description Default
text Optional[str]

The text to extract the first line from (None treated as empty)

required

Returns:

Name Type Description
str str

The first non-empty line, or empty string if no non-empty lines found

Raises:

Type Description
ValueError

If first non-empty line starts with spaces

Source code in clak/core/descriptors.py
def first_doc_line(text: Optional[str]) -> str:
    """Get the first non-empty line from a text string.

    Args:
        text: The text to extract the first line from (None treated as empty)

    Returns:
        str: The first non-empty line, or empty string if no non-empty lines found

    Raises:
        ValueError: If first non-empty line starts with spaces
    """
    if not text:
        return ""
    lines = text.split("\n")
    for line in lines:
        if line.strip():
            if line.startswith(" "):
                raise ValueError(
                    f"First line of docstring should not start with spaces: {line}"
                )
            return line
    return ""

prepare_docstring(text, variables=None, reindent='')

Prepare a docstring by deindenting and formatting with variables.

Parameters:

Name Type Description Default
text str

The docstring text to prepare

required
variables dict

Variables to format into the docstring

None
reindent str

String to use for reindenting

''

Returns:

Name Type Description
str Optional[str]

The prepared docstring, or None/SUPPRESS if input was None/SUPPRESS

Raises:

Type Description
KeyError

If formatting fails due to missing variables

TypeError

If variables arg is not a dict

Source code in clak/core/descriptors.py
def prepare_docstring(
    text: Optional[str], variables: Optional[Dict[str, Any]] = None, reindent: str = ""
) -> Optional[str]:
    """Prepare a docstring by deindenting and formatting with variables.

    Args:
        text (str): The docstring text to prepare
        variables (dict, optional): Variables to format into the docstring
        reindent (str, optional): String to use for reindenting

    Returns:
        str: The prepared docstring, or None/SUPPRESS if input was None/SUPPRESS

    Raises:
        KeyError: If formatting fails due to missing variables
        TypeError: If variables arg is not a dict
    """

    variables = variables or {}
    if not isinstance(variables, dict):
        raise TypeError(f"Got {type(variables)} instead of dict")

    if text is None:
        return None
    if text == SUPPRESS:
        return SUPPRESS

    text = deindent_docstring(text, reindent=reindent)
    try:
        text = text.format(**variables)
    except KeyError as err:
        logger.exception(
            "Error formatting docstring: %s; variables=%s; text=%s",
            err,
            variables,
            text,
        )
        raise

    return text

Build / dispatch / execute:

clak.core.parser

Clak parser: ParserNode build, dispatch, and execute.

Descriptors (Argument, Arg, Opt, SubParser, MetaSetting, docstring helpers) live in clak.core.descriptors and are re-exported here for compatibility.

Canonical public names: Parser, Argument, Command (alias of SubParser). Optional helpers: Arg (positionals), Opt (flags). Instantiate a root Parser to parse and run; it calls dispatch() automatically unless parse=False.

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()