Coverage for .tox/cov/lib/python3.13/site-packages/confattr/quickstart.py: 100%

93 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-26 19:18 +0200

1#!./runmodule.sh 

2 

3''' 

4Importing this module defines several settings which are useful in many applications and provides the :class:`~confattr.quickstart.ConfigManager` class. 

5This allows for an easy configuration setup in exchange for flexibility. 

6If you want more flexibility you can either subclass :class:`~confattr.quickstart.ConfigManager` and override specific methods or you do not import this module at all and use :class:`~confattr.configfile.ConfigFile` and :class:`~confattr.config.Config` directly. 

7''' 

8 

9import sys 

10import argparse 

11from types import ModuleType 

12from collections.abc import Callable 

13 

14from . import Config, ConfigFile, UiNotifier, Message, NotificationLevel 

15from .types import SubprocessCommandWithAlternatives as Command, OptionalExistingDirectory, TYPE_CONTEXT 

16from .utils import CallAction, HelpFormatter 

17from .configfile import Include, HelpWriter 

18from .formatters import List, Primitive 

19 

20 

21Include.home = Config('include.home', OptionalExistingDirectory(''), help="The directory where the include command looks for relative paths. An empty string is the default, the directory where the config file is located.") 

22Include.extensions = Config('include.extensions', [], help="File extensions to be suggested for auto completion of the include command. An empty list means all files with any extensions are suggested. The extensions should include the leading full stop. The check is case insensitive.", type=List(Primitive(str))) 

23 

24class ConfigManager: 

25 

26 #: A setting allowing the user to specify how to open text files, e.g. the config file 

27 editor = Config('editor', Command.editor(visual=False), help="The editor to be used when opening the config file.") 

28 

29 #: A setting allowing the user to specify which messages they want to see when loading a config file with :meth:`~confattr.quickstart.ConfigManager.load` 

30 notification_level_config = Config('notification-level.config-file', NotificationLevel.ERROR) 

31 

32 #: A setting allowing the user to specify which messages they want to see when parsing a line with :meth:`~confattr.quickstart.ConfigManager.parse_line` 

33 notification_level_ui = Config('notification-level.user-interface', NotificationLevel.INFO) 

34 

35 #: Can be used to print messages to the user interface, uses :attr:`~confattr.quickstart.ConfigManager.notification_level_ui` 

36 ui_notifier: UiNotifier 

37 

38 def __init__(self, appname: str, version: str, doc: 'str|None', *, 

39 changelog_url: 'str|None' = None, 

40 show_python_version_in_version: bool = False, 

41 show_additional_modules_in_version: 'list[ModuleType]' = [], 

42 ) -> None: 

43 ''' 

44 Defines two :class:`~confattr.configfile.ConfigFile` instances with separately configurable notification levels for :meth:`~confattr.quickstart.ConfigManager.load` and :meth:`~confattr.quickstart.ConfigManager.parse_line`. 

45 This object also provides :meth:`~confattr.quickstart.ConfigManager.create_argument_parser` to create an :class:`argparse.ArgumentParser` with commonly needed arguments. 

46 Note that all :class:`~confattr.config.Config` instances must be created before instantiating this class. 

47 

48 :param appname: The name of the app, used to initialize :class:`~confattr.configfile.ConfigFile` and when printing the version number 

49 :param version: The version of the app, used when printing the version number 

50 :param doc: The package doc string, used as description when creating an :class:`~argparse.ArgumentParser` 

51 

52 :param changelog_url: The URL to the change log, used when printing the version number 

53 :param show_additional_modules_in_version: A sequence of libraries which should be included when printing the version number 

54 :param show_python_version_in_version: If true: include the Python version number when printing the version number 

55 ''' 

56 self.appname = appname 

57 self.version = version 

58 self.doc = doc 

59 self.changelog_url = changelog_url 

60 self.show_python_version_in_version = show_python_version_in_version 

61 self.show_additional_modules_in_version = show_additional_modules_in_version 

62 

63 self.config_file = ConfigFile(appname=appname, notification_level=type(self).notification_level_config) 

64 self.user_interface = ConfigFile(appname=appname, notification_level=type(self).notification_level_ui, show_line_always=False) 

65 self.user_interface.command_dict['include'].config_file = self.config_file 

66 self.ui_notifier = self.user_interface.ui_notifier 

67 

68 def set_ui_callback(self, callback: 'Callable[[Message], None]') -> None: 

69 ''' 

70 Register a user interface notification callback function to all :class:`~confattr.configfile.ConfigFile` instances created by this object. 

71 

72 See :meth:`ConfigFile.set_ui_callback() <confattr.configfile.ConfigFile.set_ui_callback>`. 

73 ''' 

74 self.config_file.set_ui_callback(callback) 

75 self.user_interface.set_ui_callback(callback) 

76 

77 def print_errors_without_ui(self) -> None: 

78 ''' 

79 Call :meth:`~confattr.quickstart.ConfigManager.set_ui_callback` with :func:`print` so that all messages are printed to the terminal. 

80 ''' 

81 self.set_ui_callback(print) 

82 

83 

84 # ------- config file ------- 

85 

86 def load(self) -> bool: 

87 ''' 

88 Load settings from config file and environment variables. 

89 

90 :return: true if no errors have occurred, false if one or more errors have occurred 

91 ''' 

92 return self.config_file.load() 

93 

94 def parse_line(self, ln: str) -> bool: 

95 ''' 

96 Parse a line from the user interface. 

97 

98 :return: true if no errors have occurred, false if one or more errors have occurred 

99 ''' 

100 return self.user_interface.parse_line(ln) 

101 

102 def edit_config(self, *, context: TYPE_CONTEXT, update: bool = False) -> None: 

103 ''' 

104 Open the config file in a text editor. 

105 

106 If the config file does not exist it is created first. 

107 The text editor can be configured with :attr:`~confattr.quickstart.ConfigManager.editor`. 

108 

109 :param context: Returns a context manager which can be used to stop and start an urwid screen. 

110 It takes the command to be executed as argument so that it can log the command 

111 and it returns the command to be executed so that it can modify the command, 

112 e.g. processing and intercepting some environment variables. 

113 :param update: Load and rewrite the config file if it is already existing. 

114 ''' 

115 if update: 

116 self.config_file.load() 

117 self.editor \ 

118 .replace(Command.WC_FILE_NAME, self.config_file.save(if_not_existing=not update)) \ 

119 .run(context=context) 

120 self.config_file.load() 

121 

122 def get_save_path(self) -> str: 

123 ''' 

124 :return: The first existing and writable file returned by :meth:`~confattr.configfile.ConfigFile.iter_config_paths` or the first path if none of the files are existing and writable. 

125 ''' 

126 return self.config_file.get_save_path() 

127 

128 def save(self, if_not_existing: bool = False) -> str: 

129 ''' 

130 Save the current values of all settings to the file returned by :meth:`~confattr.configfile.ConfigFile.get_save_path`. 

131 Directories are created as necessary. 

132 

133 :param if_not_existing: Do not overwrite the file if it is already existing. 

134 :return: The path to the file which has been written 

135 ''' 

136 

137 return self.config_file.save(if_not_existing=if_not_existing) 

138 

139 

140 # ------- creating an argument parser ------- 

141 

142 def create_argument_parser(self) -> argparse.ArgumentParser: 

143 ''' 

144 Create an :class:`argparse.ArgumentParser` with arguments to display the version, edit the config file and choose a config file 

145 by calling :meth:`~confattr.quickstart.ConfigManager.create_empty_argument_parser`, :meth:`~confattr.quickstart.ConfigManager.add_config_help_argument`, :meth:`~confattr.quickstart.ConfigManager.add_version_argument` and :meth:`~confattr.quickstart.ConfigManager.add_config_related_arguments`. 

146 ''' 

147 p = self.create_empty_argument_parser() 

148 self.add_config_help_argument(p) 

149 self.add_version_argument(p) 

150 self.add_config_related_arguments(p) 

151 return p 

152 

153 def create_empty_argument_parser(self) -> argparse.ArgumentParser: 

154 ''' 

155 Create an :class:`argparse.ArgumentParser` with the :paramref:`~confattr.quickstart.ConfigManager.doc` passed to the constructor as description and :class:`~confattr.utils.HelpFormatter` as formatter_class but without custom arguments. 

156 ''' 

157 return argparse.ArgumentParser(description=self.doc, formatter_class=HelpFormatter) 

158 

159 def add_config_help_argument(self, p: argparse.ArgumentParser) -> argparse.ArgumentParser: 

160 ''' 

161 Add a ``--help-config`` argument to an :class:`argparse.ArgumentParser`. 

162 

163 This is not part of :meth:`~confattr.quickstart.ConfigManager.add_config_related_arguments` so that this argument can be insterted between ``--help`` and ``--version``. 

164 

165 :return: The same object that has been passed in 

166 ''' 

167 p.add_argument('-H', '--help-config', action=CallAction, callback=self.print_config_help_and_exit) 

168 return p 

169 

170 def add_version_argument(self, p: argparse.ArgumentParser) -> argparse.ArgumentParser: 

171 ''' 

172 Add an argument to print the version and exit to an :class:`argparse.ArgumentParser`. 

173 

174 :return: The same object that has been passed in 

175 ''' 

176 p.add_argument('-v', '--version', action=CallAction, callback=self.print_version_and_exit) 

177 return p 

178 

179 def add_config_related_arguments(self, p: argparse.ArgumentParser) -> argparse.ArgumentParser: 

180 ''' 

181 Add arguments related to the config file to an :class:`argparse.ArgumentParser`. 

182 

183 :return: The same object that has been passed in 

184 ''' 

185 p.add_argument('-e', '--edit-config', action=CallAction, callback=self.edit_config_and_exit) 

186 p.add_argument('-E', '--update-and-edit-config', action=CallAction, callback=self.update_config_and_exit) 

187 p.add_argument('-c', '--config', action=CallAction, callback=self.select_config_file, nargs=1) 

188 return p 

189 

190 

191 # ------- printing version and help ------- 

192 

193 def print_version(self) -> None: 

194 versions: 'list[tuple[str, object]]' = [] 

195 versions.append((self.appname, self.version)) 

196 if self.show_python_version_in_version: 

197 versions.append(("python", sys.version)) 

198 for mod in self.show_additional_modules_in_version: 

199 versions.append((mod.__name__, getattr(mod, '__version__', "(unknown version)"))) 

200 

201 for name, version in versions: 

202 print("%s %s" % (name, version)) 

203 

204 if self.changelog_url: 

205 print("change log:", self.changelog_url) 

206 

207 def print_config_help(self) -> None: 

208 self.config_file.write_help(HelpWriter(sys.stdout)) 

209 

210 

211 # ------- argument parser option callbacks ------- 

212 

213 def print_version_and_exit(self) -> None: 

214 '''show the version and exit''' 

215 self.print_version() 

216 sys.exit() 

217 

218 def print_config_help_and_exit(self) -> None: 

219 '''show the config help and exit''' 

220 self.print_config_help() 

221 sys.exit() 

222 

223 def edit_config_and_exit(self) -> None: 

224 '''edit the config file and exit''' 

225 self.edit_config(context=None, update=False) 

226 self.print_errors_without_ui() 

227 sys.exit() 

228 

229 def update_config_and_exit(self) -> None: 

230 '''rewrite the config file, open it for editing and exit''' 

231 self.edit_config(context=None, update=True) 

232 self.print_errors_without_ui() 

233 sys.exit() 

234 

235 def select_config_file(self, path: str) -> None: 

236 '''use this config file instead of the default''' 

237 ConfigFile.config_path = path