Source code for betty.cli

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

from __future__ import annotations

import logging
import sys
from contextlib import contextmanager
from typing import (
    Iterator,
    TYPE_CHECKING,
)

import click

from betty import about
from betty.asyncio import wait_to_thread
from betty.error import UserFacingError
from betty.locale import DEFAULT_LOCALIZER
from betty.logging import CliHandler
from betty.plugin import PluginRepository, PluginNotFound

if TYPE_CHECKING:
    from collections.abc import Callable
    from betty.cli.commands import Command


[docs] @contextmanager def catch_exceptions() -> Iterator[None]: """ Catch and log all exceptions. """ try: yield except KeyboardInterrupt: print("Quitting...") # noqa T201 sys.exit(0) except Exception as e: logger = logging.getLogger(__name__) if isinstance(e, UserFacingError): logger.error(e.localize(DEFAULT_LOCALIZER)) else: logger.exception(e) sys.exit(1)
class _BettyCommands(click.MultiCommand): _bootstrapped = False commands: PluginRepository[Command] def _bootstrap(self) -> None: if not self._bootstrapped: logging.getLogger().addHandler(CliHandler()) self._bootstrapped = True @catch_exceptions() 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()) ] @catch_exceptions() 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 def _build_init_ctx_verbosity( betty_logger_level: int, root_logger_level: int | None = None, ) -> Callable[[click.Context, click.Option | click.Parameter | None, bool], None]: def _init_ctx_verbosity( _: click.Context, __: click.Option | click.Parameter | None = None, is_verbose: bool = False, ) -> None: if is_verbose: for logger_name, logger_level in ( ("betty", betty_logger_level), (None, root_logger_level), ): logger = logging.getLogger(logger_name) if ( logger_level is not None and logger.getEffectiveLevel() > logger_level ): logger.setLevel(logger_level) return _init_ctx_verbosity @click.command( cls=_BettyCommands, # Set an empty help text so Click does not automatically use the function's docstring. help="", ) @click.option( "-v", "--verbose", is_eager=True, default=False, is_flag=True, help="Show verbose output, including informative log messages.", callback=_build_init_ctx_verbosity(logging.INFO), ) @click.option( "-vv", "--more-verbose", "more_verbose", is_eager=True, default=False, is_flag=True, help="Show more verbose output, including debug log messages.", callback=_build_init_ctx_verbosity(logging.DEBUG), ) @click.option( "-vvv", "--most-verbose", "most_verbose", is_eager=True, default=False, is_flag=True, help="Show most verbose output, including all log messages.", callback=_build_init_ctx_verbosity(logging.NOTSET, logging.NOTSET), ) @click.version_option( wait_to_thread(about.version_label()), message=wait_to_thread(about.report()), prog_name="Betty", ) def main(verbose: bool, more_verbose: bool, most_verbose: bool) -> None: """ Launch Betty's Command-Line Interface. """ pass # pragma: no cover