Coverage for src/twofas/cli_settings.py: 100%

59 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2025-01-12 18:05 +0100

1""" 

2This file deals with managing settings for 2fas. 

3""" 

4 

5import typing 

6from pathlib import Path 

7from typing import Any 

8 

9import tomli_w 

10from configuraptor import TypedConfig, asdict, beautify, singleton 

11from configuraptor.core import convert_key 

12 

13config = Path("~/.config").expanduser() 

14config.mkdir(exist_ok=True) 

15DEFAULT_SETTINGS = config / "2fas.toml" 

16DEFAULT_SETTINGS.touch(exist_ok=True) 

17 

18CONFIG_KEY = "tool.2fas" 

19 

20 

21def expand_path(file: str | Path | None) -> str: 

22 """ 

23 Expand ~/... into /home/<user>/... 

24 """ 

25 if not file: 

26 return "" 

27 

28 return str(Path(file).expanduser().absolute()) 

29 

30 

31def expand_paths(paths: typing.Iterable[str]) -> list[str]: 

32 """ 

33 Expand multiple paths. 

34 """ 

35 return [expand_path(f) for f in paths] 

36 

37 

38@beautify 

39class CliSettings(TypedConfig, singleton.Singleton): 

40 """ 

41 Class for the ~/.config/2fas.toml settings file. 

42 """ 

43 

44 files: list[str] | None 

45 default_file: str | None 

46 auto_verbose: bool = False 

47 

48 def add_file(self, filename: str | None, _config_file: str | Path = DEFAULT_SETTINGS) -> str | None: 

49 """ 

50 Add a new 2fas file to the configs history list. 

51 """ 

52 if not filename: 

53 return None 

54 

55 filename = expand_path(filename) 

56 

57 files = self.files or [] 

58 if filename not in files: 

59 files.append(filename) 

60 

61 set_cli_setting("files", expand_paths(files), _config_file) 

62 

63 self.files = expand_paths(files) 

64 return expand_path(filename) 

65 

66 def remove_file(self, filenames: str | typing.Iterable[str], _config_file: str | Path = DEFAULT_SETTINGS) -> None: 

67 """ 

68 Remove a known 2fas file from the config's history list. 

69 """ 

70 if isinstance(filenames, str | Path): 

71 filenames = [filenames] 

72 

73 filenames_to_remove = set(expand_paths(filenames)) 

74 current_files = expand_paths(self.files or []) 

75 files = [_ for _ in current_files if _ not in filenames_to_remove] 

76 

77 if expand_path(self.default_file) in filenames_to_remove: 

78 new_default = files[0] if files else None 

79 set_cli_setting("default-file", new_default, _config_file) 

80 self.default_file = new_default 

81 

82 set_cli_setting("files", files, _config_file) 

83 self.files = files 

84 

85 

86def load_cli_settings(input_file: str | Path = DEFAULT_SETTINGS, **overwrite: Any) -> CliSettings: 

87 """ 

88 Load the config file into a CliSettings instance. 

89 """ 

90 return CliSettings.load([input_file, overwrite], key=CONFIG_KEY) 

91 

92 

93def get_cli_setting(key: str, filename: str | Path = DEFAULT_SETTINGS) -> typing.Any: 

94 """ 

95 Get a setting from the config file. 

96 """ 

97 key = convert_key(key) 

98 settings = load_cli_settings(filename) 

99 return getattr(settings, key) 

100 

101 

102def set_cli_setting(key: str, value: typing.Any, filename: str | Path = DEFAULT_SETTINGS) -> None: 

103 """ 

104 Update a setting in the config file. 

105 """ 

106 filepath = Path(filename) 

107 key = convert_key(key) 

108 

109 settings = load_cli_settings(filepath) 

110 settings.update(**{key: value}, _convert_types=True) 

111 

112 inner_data = asdict( 

113 settings, 

114 with_top_level_key=False, 

115 ) 

116 

117 # toml can't deal with None, so skip those: 

118 inner_data = {k: v for k, v in inner_data.items() if v is not None} 

119 outer_data = {"tool": {"2fas": inner_data}} 

120 

121 filepath.write_text(tomli_w.dumps(outer_data))