Coverage for src/somesy/cli/util.py: 98%
46 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-11-03 13:13 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-11-03 13:13 +0000
1"""Utility functions for CLI commands."""
3import logging
4import traceback
5from typing import TypedDict
7import typer
8import wrapt
9from rich.markup import escape
10from rich.pretty import pretty_repr
12from somesy.core.core import discover_input
13from somesy.core.log import SomesyLogLevel, get_log_level, set_log_level
14from somesy.core.models import SomesyConfig, SomesyInput
16logger = logging.getLogger("somesy")
19class FileArgConfig(TypedDict):
20 """Keyword arguments shared by Typer file options."""
22 file_okay: bool
23 dir_okay: bool
24 writable: bool
25 readable: bool
26 resolve_path: bool
29class ExistingFileArgConfig(FileArgConfig):
30 """File option arguments that require an existing file."""
32 exists: bool
35# configuration dicts for CLI file arguments
36file_arg_config: FileArgConfig = {
37 "file_okay": True,
38 "dir_okay": False,
39 "writable": True,
40 "readable": True,
41 "resolve_path": True,
42}
43existing_file_arg_config: ExistingFileArgConfig = {
44 "file_okay": True,
45 "dir_okay": False,
46 "writable": True,
47 "readable": True,
48 "resolve_path": True,
49 "exists": True,
50}
51existing_dir_arg_config: ExistingFileArgConfig = {
52 "file_okay": False,
53 "dir_okay": True,
54 "writable": True,
55 "readable": True,
56 "resolve_path": True,
57 "exists": True,
58}
61@wrapt.decorator
62def wrap_exceptions(wrapped, instance, args, kwargs):
63 """Format and log exceptions for cli commands."""
64 try:
65 return wrapped(*args, **kwargs)
67 except (typer.Abort, typer.Exit, typer.exceptions.TyperException):
68 raise
69 except Exception as e:
70 # Escape the error message to prevent Rich from misinterpreting it
71 escaped_error_message = escape(str(e))
72 escaped_traceback = escape(traceback.format_exc())
74 logger.error(f"[bold red]Error: {escaped_error_message}[/bold red]")
75 logger.debug(f"[red]{escaped_traceback}[/red]")
76 raise typer.Exit(code=1) from e
79def resolved_somesy_input(**cli_args) -> SomesyInput:
80 """Return a combined `SomesyInput` based on config file and passed CLI args.
82 Will also adjust log levels accordingly.
83 """
84 # figure out what input file to use
85 input_file = discover_input(cli_args.pop("input_file", None))
87 # create config based on passed arguments
88 passed_args = {k: v for k, v in cli_args.items() if v is not None}
89 somesy_conf = SomesyConfig(input_file=input_file, **passed_args)
91 # cli_log_level is None if the user did not pass a log level (-> "default")
92 cli_log_level: SomesyLogLevel | None = get_log_level()
94 if cli_log_level is not None:
95 # update log level flags if cli log level was set
96 somesy_conf.update_log_level(cli_log_level)
98 somesy_input: SomesyInput = somesy_conf.get_input()
100 if cli_log_level is None:
101 # no cli log level -> set it according to the loaded configuration
102 set_log_level(somesy_input.config.log_level())
104 logger.debug(
105 f"Combined config (Defaults + File + CLI):\n{pretty_repr(somesy_input.config)}"
106 )
107 return somesy_input