Skip to content

CLI module

The CLI module defines the boldi CLI and the classes necessary for other modules to define boldi CLI subcommands via an internal plugin system.

Install

Boldi's CLI module is distributed as the boldi-cli Python package, thus to install it, run:

pip install boldi-cli

...or add "boldi-cli" as a dependency to your project.

Run

The Boldi CLI can be invoked using the boldi command:

boldi --help

Subcommands

boldi

Same as boldi help.

boldi help

Shows the help message for a subcommand.

boldi dev

Subcommand used for developing Boldi's Python libraries. Available if the boldi-dev Python package is installed.

See: Development module.

Plugins

Subcommands for boldi (such as boldi help or boldi dev) can be defined using plugins.

The plugin must be declared as a Python package entry point, with the following properties:

  • The entry point group must be boldi.cli.action.
  • The entry point name will be used as the name of the subcommand.
  • The entry point must refer to function that takes the following arguments:
    • The class's __init__ method must implement the subcommand argument parser.
    • The class must call subparser.set_defaults(action=<some-function>) to perform an action.

API

boldi.cli

CliCtx

Bases: Ctx

Extends Ctx with a console feature for rich CLI output.

Source code in pkg/boldi-cli/boldi/cli.py
@dataclass
class CliCtx(Ctx):
    """Extends [`Ctx`][boldi.ctx.Ctx] with a console feature for rich CLI output."""

    console: Console = field(default_factory=_CliCtxDefaultConsole)
    """A rich console that outputs to `self.stderr` by default."""

    parser: ArgumentParser = field(default_factory=ArgumentParser)
    """The root [`argparse.ArgumentParser`][] for the `boldi` CLI."""

    verbose: bool = field(default=False)
    """Whether to enable verbose output."""

    def __post_init__(self):
        if isinstance(self.console, _CliCtxDefaultConsole):
            self.console = Console(file=self.stderr, theme=_THEME, highlight=False)

    def msg(self, *args, **kwargs):
        """Prints a message to `self.console`."""
        self.console.print(*args, **kwargs)

    msg_info = partialmethod(msg, style="info")
    msg_pass = partialmethod(msg, style="pass")
    msg_PASS = partialmethod(msg, style="PASS")
    msg_warn = partialmethod(msg, style="warn")
    msg_WARN = partialmethod(msg, style="WARN")
    msg_fail = partialmethod(msg, style="fail")
    msg_FAIL = partialmethod(msg, style="FAIL")
argv: list[str] = field(default_factory=lambda: sys.argv)

Replacement for sys.argv.

console: Console = field(default_factory=_CliCtxDefaultConsole)

A rich console that outputs to self.stderr by default.

cwd: Path = field(default_factory=Path.cwd)

Replacement for pathlib.Path.cwd.

env: MutableMapping[str, str] = field(default_factory=lambda: os.environ)

Replacement for os.environ.

msg_FAIL = partialmethod(msg, style='FAIL')
msg_PASS = partialmethod(msg, style='PASS')
msg_WARN = partialmethod(msg, style='WARN')
msg_fail = partialmethod(msg, style='fail')
msg_info = partialmethod(msg, style='info')
msg_pass = partialmethod(msg, style='pass')
msg_warn = partialmethod(msg, style='warn')
parser: ArgumentParser = field(default_factory=ArgumentParser)

The root argparse.ArgumentParser for the boldi CLI.

stack: ExitStack = field(default_factory=ExitStack)

An ExitStack provided for convenience.

stderr: TextIO = field(default_factory=lambda: sys.stderr)

Replacement for sys.stderr.

stdin: TextIO = field(default_factory=lambda: sys.stdin)

Replacement for sys.stdin.

stdout: TextIO = field(default_factory=lambda: sys.stdout)

Replacement for sys.stdout.

verbose: bool = field(default=False)

Whether to enable verbose output.

__enter__() -> Self

Enter the context of self.stack.

Source code in pkg/boldi-ctx/boldi/ctx.py
def __enter__(self) -> Self:
    """Enter the context of `self.stack`."""
    self.stack.__enter__()
    return self
__exit__(*exc_info) -> bool | None

Exit the context of self.stack.

Source code in pkg/boldi-ctx/boldi/ctx.py
def __exit__(self, *exc_info) -> bool | None:
    """Exit the context of `self.stack`."""
    return self.stack.__exit__(*exc_info)
__init__(stack: ExitStack = ExitStack(), stdin: TextIO = lambda: sys.stdin(), stdout: TextIO = lambda: sys.stdout(), stderr: TextIO = lambda: sys.stderr(), argv: list[str] = lambda: sys.argv(), env: MutableMapping[str, str] = lambda: os.environ(), cwd: Path = Path.cwd(), console: Console = _CliCtxDefaultConsole(), parser: ArgumentParser = ArgumentParser(), verbose: bool = False) -> None
__post_init__()
Source code in pkg/boldi-cli/boldi/cli.py
def __post_init__(self):
    if isinstance(self.console, _CliCtxDefaultConsole):
        self.console = Console(file=self.stderr, theme=_THEME, highlight=False)
chdir(path: Path)

Change the current working directory to path and restore later via self.stack.

Source code in pkg/boldi-ctx/boldi/ctx.py
def chdir(self, path: Path):
    """Change the current working directory to `path` and restore later via `self.stack`."""
    self.stack.callback(setattr, self, "cwd", self.cwd)
    self.stack.enter_context(chdir(path))
    self.cwd = path
msg(*args, **kwargs)

Prints a message to self.console.

Source code in pkg/boldi-cli/boldi/cli.py
def msg(self, *args, **kwargs):
    """Prints a message to `self.console`."""
    self.console.print(*args, **kwargs)
run(*args: Union[str, List[Any]], **kwargs: Unpack[RunArgs]) -> subprocess.CompletedProcess

Run a subprocess using the provided command line arguments and updated defaults.

Parameters:

  • args (Union[str, List[Any]], default: () ) –

    Command line arguments, provided as positional arguments. As defined in boldi.proc.args_iter.

  • kwargs (Unpack[RunArgs], default: {} ) –

    Arguments to subprocess.run. Defaults to check=True, text=True, and values set in self.{stdin,stdout,stderr,env,cwd}, unless otherwise set by the caller.

Returns:

Source code in pkg/boldi-ctx/boldi/ctx.py
def run(self, *args: Union[str, List[Any]], **kwargs: Unpack[RunArgs]) -> subprocess.CompletedProcess:
    """
    Run a subprocess using the provided command line arguments and updated defaults.

    Args:
        args: Command line arguments, provided as positional arguments.
            As defined in [`boldi.proc.args_iter`][].
        kwargs: Arguments to [`subprocess.run`][].
            Defaults to `check=True`, `text=True`, and values set in `self.{stdin,stdout,stderr,env,cwd}`,
            unless otherwise set by the caller.

    Returns:
        Completed process object.
    """
    self._set_run_kwargs(**kwargs)
    return _run(*args, **kwargs)
run_py(*args: Union[str, List[Any]], **kwargs: Unpack[RunArgs]) -> subprocess.CompletedProcess

Run a subprocess using the current Python interpreter, the provided command line arguments and updated defaults.

Parameters:

Returns:

Source code in pkg/boldi-ctx/boldi/ctx.py
def run_py(self, *args: Union[str, List[Any]], **kwargs: Unpack[RunArgs]) -> subprocess.CompletedProcess:
    """
    Run a subprocess using the current Python interpreter, the provided command line arguments and updated defaults.

    Args:
        args: Command line arguments, provided as positional arguments.
            As defined in [`boldi.proc.args_iter`][].
        kwargs: Arguments to [`subprocess.run`][]. As defined in [`run`][boldi.ctx.Ctx.run].

    Returns:
        Completed process object.
    """
    self._set_run_kwargs(**kwargs)
    return _run_py(*args, **kwargs)

CliUsageException

Bases: Exception

Raised when a CLI usage error is encountered.

Source code in pkg/boldi-cli/boldi/cli.py
class CliUsageException(Exception):
    """Raised when a CLI usage error is encountered."""

    pass

error_handler(ctx: CliCtx)

Context manager that catches all exceptions and converts them to console messages.

Source code in pkg/boldi-cli/boldi/cli.py
@contextmanager
def error_handler(ctx: CliCtx):
    """Context manager that catches all exceptions and converts them to console messages."""
    try:
        yield

    except CliUsageException as exc:
        ctx.msg_FAIL("Error:", exc.args[0])
        ctx.msg_warn("[bold]Note:[/b]", exc.args[1])  # TODO use Exception notes for Python 3.11+
        if ctx.verbose:
            ctx.console.print(_rich_traceback_from_exception(exc))

        exit(1)

    except Exception as exc:
        ctx.msg_FAIL(f"INTERNAL ERROR: {type(exc).__name__}:", *exc.args)
        ctx.msg_fail("This is a bug, please report it.")
        if ctx.verbose:
            ctx.console.print(_rich_traceback_from_exception(exc))
        else:
            ctx.msg_info("Use [bold]--verbose[/] or [bold]-v[/] for more info.")

        exit(2)

esc(obj: object) -> str

Source code in pkg/boldi-cli/boldi/cli.py
def esc(obj: object) -> str:
    return rich_escape(str(obj))

main(ctx: CliCtx | None = None)

Main entry point that implements the boldi CLI.

Source code in pkg/boldi-cli/boldi/cli.py
def main(ctx: CliCtx | None = None):
    """Main entry point that implements the `boldi` CLI."""
    ctx = ctx or CliCtx()
    with ctx:
        ctx.stack.enter_context(error_handler(ctx))

        parser = ArgumentParser(prog="boldi", exit_on_error=False)
        parser.set_defaults(action=partial(parser.print_help, ctx.stderr))
        parser.add_argument("--verbose", "-v", action="store_true", help="enable verbose output")
        parser.add_argument("--chdir", "-C", type=Path, help="change working directory")

        subparsers = parser.add_subparsers(title="action", help="action to run")
        for plugin in boldi.plugins.load("boldi.cli.action"):  # type: ignore[type-abstract]
            subparser = subparsers.add_parser(plugin.name)
            subparser.set_defaults(action=partial(subparser.print_help, ctx.stderr))
            _call_with(plugin.impl, ctx=ctx, subparser=subparser)

        try:
            args = vars(parser.parse_args(ctx.argv[1:]))
        except ArgumentError as exc:
            help = parser.format_help().strip()
            if exc.argument_name:
                raise CliUsageException(exc.argument_name + ": " + exc.message, help) from exc
            else:
                raise CliUsageException(exc.message, help) from exc

        ctx.verbose = args.pop("verbose")

        if chdir := args.pop("chdir", None):
            if not chdir.is_dir():
                raise CliUsageException(
                    f"{esc(chdir)} is not a directory",
                    "set [bold]--chdir[/]/[bold]-C[/] to a valid directory or omit it.",
                )
            ctx.chdir(chdir)

        action: Callable[..., None] | None = args.pop("action", None)
        if action and callable(action):
            _call_with(action, **args)
        else:
            parser.print_help(ctx.stderr)