Coverage for src/somesy/cli/util.py: 98%
43 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-04 11:35 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-04 11:35 +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}
53@wrapt.decorator
54def wrap_exceptions(wrapped, instance, args, kwargs):
55 """Format and log exceptions for cli commands."""
56 try:
57 return wrapped(*args, **kwargs)
59 except Exception as e:
60 # Escape the error message to prevent Rich from misinterpreting it
61 escaped_error_message = escape(str(e))
62 escaped_traceback = escape(traceback.format_exc())
64 logger.error(f"[bold red]Error: {escaped_error_message}[/bold red]")
65 logger.debug(f"[red]{escaped_traceback}[/red]")
66 raise typer.Exit(code=1) from e
69def resolved_somesy_input(**cli_args) -> SomesyInput:
70 """Return a combined `SomesyInput` based on config file and passed CLI args.
72 Will also adjust log levels accordingly.
73 """
74 # figure out what input file to use
75 input_file = discover_input(cli_args.pop("input_file", None))
77 # create config based on passed arguments
78 passed_args = {k: v for k, v in cli_args.items() if v is not None}
79 somesy_conf = SomesyConfig(input_file=input_file, **passed_args)
81 # cli_log_level is None if the user did not pass a log level (-> "default")
82 cli_log_level: SomesyLogLevel | None = get_log_level()
84 if cli_log_level is not None:
85 # update log level flags if cli log level was set
86 somesy_conf.update_log_level(cli_log_level)
88 somesy_input: SomesyInput = somesy_conf.get_input()
90 if cli_log_level is None:
91 # no cli log level -> set it according to the loaded configuration
92 set_log_level(somesy_input.config.log_level())
94 logger.debug(
95 f"Combined config (Defaults + File + CLI):\n{pretty_repr(somesy_input.config)}"
96 )
97 return somesy_input