Source code for betty.cli

"""
Provide the Command Line Interface.
"""

from __future__ import annotations

import logging
from logging import (
    Handler,
    CRITICAL,
    ERROR,
    WARNING,
    INFO,
    DEBUG,
    NOTSET,
    LogRecord,
)
from sys import stderr
from typing import TYPE_CHECKING, final, IO, Any, TypeVar

import click
from click import Context
from typing_extensions import override, ClassVar

from betty import about
from betty.asyncio import wait_to_thread
from betty.error import UserFacingError
from betty.plugin import PluginRepository, PluginNotFound
from betty.cli.commands import BettyCommand

if TYPE_CHECKING:
    from betty.locale.localizer import Localizer
    from betty.assertion import Assertion
    from betty.cli.commands import Command

_ValueT = TypeVar("_ValueT")
_ReturnT = TypeVar("_ReturnT")


[docs] def assertion_to_value_proc( assertion: Assertion[_ValueT, _ReturnT], localizer: Localizer ) -> Assertion[_ValueT, _ReturnT]: """ Convert an :py:class:`betty.assertion.Assertion` to a Click ``value_proc`` callback. """ def _assert(value: _ValueT) -> _ReturnT: try: return assertion(value) except UserFacingError as error: message = error.localize(localizer) logging.getLogger(__name__).debug(message) raise click.BadParameter(message) from None return _assert
@final class _ClickHandler(Handler): """ Output log records to stderr with :py:func:`click.secho`. """ COLOR_LEVELS = { CRITICAL: "red", ERROR: "red", WARNING: "yellow", INFO: "green", DEBUG: "white", NOTSET: "white", } def __init__(self, stream: IO[Any] = stderr): super().__init__(-1) self._stream = stream @override def emit(self, record: LogRecord) -> None: click.secho(self.format(record), file=self._stream, fg=self._color(record)) def _color(self, record: LogRecord) -> str: for level, color in self.COLOR_LEVELS.items(): if record.levelno >= level: return color return self.COLOR_LEVELS[NOTSET] class _BettyCommands(BettyCommand, click.MultiCommand): terminal_width: ClassVar[int | None] = None _bootstrapped = False commands: PluginRepository[Command] def _bootstrap(self) -> None: if not self._bootstrapped: logging.getLogger().addHandler(_ClickHandler()) self._bootstrapped = True @override def list_commands(self, ctx: click.Context) -> list[str]: from betty.cli import commands self._bootstrap() return [ command.plugin_id() for command in wait_to_thread(commands.COMMAND_REPOSITORY.select()) ] @override def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None: from betty.cli import commands self._bootstrap() try: return wait_to_thread( commands.COMMAND_REPOSITORY.get(cmd_name) ).click_command() except PluginNotFound: return None @override def make_context( self, info_name: str | None, args: list[str], parent: Context | None = None, **extra: Any, ) -> Context: if self.terminal_width is not None: extra["terminal_width"] = self.terminal_width return super().make_context( info_name, # type: ignore[arg-type] args, parent, **extra, ) @click.command( "betty", cls=_BettyCommands, # Set an empty help text so Click does not automatically use the function's docstring. help="", ) @click.version_option( wait_to_thread(about.version_label()), message=wait_to_thread(about.report()), prog_name="Betty", ) def main() -> None: """ Launch Betty's Command-Line Interface. """ pass # pragma: no cover