Source code for betty.cli

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

from __future__ import annotations

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

from click import Context
from typing_extensions import override, ClassVar

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

if TYPE_CHECKING:
    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): terminal_width: ClassVar[int | None] = None _bootstrapped = False commands: PluginRepository[Command] def _bootstrap(self) -> None: if not self._bootstrapped: logging.getLogger().addHandler(CliHandler()) self._bootstrapped = True @override @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()) ] @override @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 @override def make_context( self, info_name: t.Optional[str], args: t.List[str], parent: t.Optional[Context] = None, **extra: t.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