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

1"""Utility functions for CLI commands.""" 

2 

3import logging 

4import traceback 

5from typing import TypedDict 

6 

7import typer 

8import wrapt 

9from rich.markup import escape 

10from rich.pretty import pretty_repr 

11 

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 

15 

16logger = logging.getLogger("somesy") 

17 

18 

19class FileArgConfig(TypedDict): 

20 """Keyword arguments shared by Typer file options.""" 

21 

22 file_okay: bool 

23 dir_okay: bool 

24 writable: bool 

25 readable: bool 

26 resolve_path: bool 

27 

28 

29class ExistingFileArgConfig(FileArgConfig): 

30 """File option arguments that require an existing file.""" 

31 

32 exists: bool 

33 

34 

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} 

51 

52 

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) 

58 

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()) 

63 

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 

67 

68 

69def resolved_somesy_input(**cli_args) -> SomesyInput: 

70 """Return a combined `SomesyInput` based on config file and passed CLI args. 

71 

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)) 

76 

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) 

80 

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() 

83 

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) 

87 

88 somesy_input: SomesyInput = somesy_conf.get_input() 

89 

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()) 

93 

94 logger.debug( 

95 f"Combined config (Defaults + File + CLI):\n{pretty_repr(somesy_input.config)}" 

96 ) 

97 return somesy_input