Skip to content

arc.cli

CLI (Command)

Class for creating multi-command CLI applications

Example

import arc

cli = arc.CLI()


@cli.command()
def c1():
    print("the first command")


@cli.command()
def c2():
    print("The second command")


cli()
Source code in arc/cli.py
class CLI(Command):
    """Class for creating multi-command CLI applications


    ## Example
    ```py
    import arc

    cli = arc.CLI()


    @cli.command()
    def c1():
        print("the first command")


    @cli.command()
    def c2():
        print("The second command")


    cli()

    ```
    """

    parser = CLIOptionsParser

    def __init__(
        self,
        name: str = None,
        state: dict = None,
        **ctx_dict,
    ):
        """
        Args:
            name: name of the CLI, will be used in the help command. If one is not provided,
                a name will be automatically discovered based on file name.
            state: dictionary of key value pairs to pass to commands
            ctx_dict: additional keyword arguments to pass to the execution context
        """
        utils.header("INIT")

        super().__init__(
            lambda: ...,
            name or utils.discover_name(),
            state,
            **ctx_dict,
        )

    def __completions__(self, info: CompletionInfo, *_args, **_kwargs):
        # Completes Command names
        if (
            (len(info.words) == 0 and info.current == "")
            or (len(info.words) == 1 and info.current == info.words[-1])
        ) and not info.current.startswith(constants.SHORT_FLAG_PREFIX):
            return [
                Completion(fullname, description=command.short_description or "")
                for command, fullname in helpers.get_all_commands(self)[1:]
            ]
        # Finds the current command and delegates completions to it
        elif len(info.words) >= 1:
            command_name = info.words[0]
            for command, fullname in helpers.get_all_commands(self):
                if fullname == command_name:
                    return get_completions(command, info)

        return []
        # Completes Global Options
        # return super().__completions__(info)

    @utils.timer("Running CLI")
    def _main(
        self,
        args: t.Union[str, list[str]] = None,
        fullname: str = None,
        **kwargs,
    ):
        kwargs = self.ctx_dict | kwargs
        kwargs["execute_callbacks"] = False

        utils.header("CLI")
        with self.create_ctx(fullname or self.name, **kwargs) as ctx:
            try:
                args = self.get_args(args)
                self.parse_args(ctx, args, allow_extra=True)
                self.execute(ctx)
                args = t.cast(list[str], ctx.extra)

                if not args:
                    logger.debug("No arguments present")
                    print(self.get_usage(ctx))
                    raise errors.Exit(1)

                subcommand_name = args.pop(0)
                command_namespace = helpers.get_command_namespace(subcommand_name)
                if not command_namespace:
                    raise errors.CommandNotFound("No command name provided", ctx)

                logger.debug("Executing subcommand: %s", subcommand_name)

                try:
                    command_chain = helpers.find_command_chain(
                        self, command_namespace, ctx
                    )
                except errors.CommandNotFound as e:
                    if config.environment == "development":
                        raise

                    print(str(e))
                    raise errors.Exit(1)

                return command_chain[-1]._main(  # pylint: disable=protected-access
                    args,
                    subcommand_name,
                    parent=ctx,
                    command_chain=command_chain,
                )

            except errors.Exit as e:
                sys.exit(e.code)

    def command(self, *args, **kwargs):
        """Alias for `Command.subcommand`

        Returns:
            Command: The subcommand's command object
        """
        return self.subcommand(*args, **kwargs)

    def options(self, callback: t.Callable) -> Command:
        self._callback = callback
        # In certain circumstances, params
        # may have already been consructed
        # we can del them to cause them to
        # rebuild
        try:
            del self.params
        except AttributeError:
            ...

        if config.environment == "development":
            self.params
            if len(self.pos_params) > 0:
                raise errors.ArgumentError(
                    f"{colorize('@cli.options', fg.YELLOW)} does not allow Argument parameters. "
                    "All arguments must be Option or Flag parameters"
                )

        return self

    def schema(self):
        return {
            "name": self.name,
            "version": self.version,
            "subcommands": {
                name: command.schema() for name, command in self.subcommands.items()
            },
        }

    @utils.timer("Autoloading")
    def autoload(self, *paths: str):
        """Attempts to autoload command objects
        into the CLI from the provided paths"""
        Autoload(paths, self).load()

parser (Parser)

Parser sub class used for global CLI options to that the inital CLI parse doesn't eat things like comand-specific --help. The first time we encounter a TokenType.VALUE to parse, the parser will exit with all additional data in extra

Source code in arc/cli.py
class CLIOptionsParser(Parser):
    """Parser sub class used for global CLI options
    to that the inital CLI parse doesn't eat things like
    comand-specific `--help`.
    The first time we encounter a `TokenType.VALUE` to parse, the parser
    will exit with all additional data in `extra`
    """

    class CommandNameEncountered(errors.ArcError):
        ...

    def parse(self, args: list[str]):
        try:
            return super().parse(args)
        except CLIOptionsParser.CommandNameEncountered:
            return self.parsed, self.extra

    def parse_value(self, token: Token):
        self.extra = [token.value] + [token.raw for token in self.tokens]
        raise CLIOptionsParser.CommandNameEncountered()

__init__(self, name=None, state=None, **ctx_dict) special

Parameters:

Name Type Description Default
name str

name of the CLI, will be used in the help command. If one is not provided, a name will be automatically discovered based on file name.

None
state dict

dictionary of key value pairs to pass to commands

None
ctx_dict

additional keyword arguments to pass to the execution context

{}
Source code in arc/cli.py
def __init__(
    self,
    name: str = None,
    state: dict = None,
    **ctx_dict,
):
    """
    Args:
        name: name of the CLI, will be used in the help command. If one is not provided,
            a name will be automatically discovered based on file name.
        state: dictionary of key value pairs to pass to commands
        ctx_dict: additional keyword arguments to pass to the execution context
    """
    utils.header("INIT")

    super().__init__(
        lambda: ...,
        name or utils.discover_name(),
        state,
        **ctx_dict,
    )

autoload(self, *paths)

Attempts to autoload command objects into the CLI from the provided paths

Source code in arc/cli.py
@utils.timer("Autoloading")
def autoload(self, *paths: str):
    """Attempts to autoload command objects
    into the CLI from the provided paths"""
    Autoload(paths, self).load()

command(self, *args, **kwargs)

Alias for Command.subcommand

Returns:

Type Description
Command

The subcommand's command object

Source code in arc/cli.py
def command(self, *args, **kwargs):
    """Alias for `Command.subcommand`

    Returns:
        Command: The subcommand's command object
    """
    return self.subcommand(*args, **kwargs)