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

1220 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-10 20:18 +0200

1#!./runmodule.sh 

2 

3''' 

4This module defines the ConfigFile class 

5which can be used to load and save config files. 

6''' 

7 

8import os 

9import shlex 

10import platform 

11import re 

12import enum 

13import argparse 

14import inspect 

15import io 

16import abc 

17import typing 

18from collections.abc import Iterable, Iterator, Sequence, Callable 

19 

20import appdirs 

21 

22from .config import Config, DictConfig, MultiConfig, ConfigId 

23from .formatters import AbstractFormatter 

24from .utils import HelpFormatter, HelpFormatterWrapper, SortedEnum, readable_quote 

25 

26if typing.TYPE_CHECKING: 

27 from typing_extensions import Unpack 

28 

29# T is already used in config.py and I cannot use the same name because both are imported with * 

30T2 = typing.TypeVar('T2') 

31 

32 

33#: If the name or an alias of :class:`~confattr.configfile.ConfigFileCommand` is this value that command is used by :meth:`ConfigFile.parse_split_line() <confattr.configfile.ConfigFile.parse_split_line>` if an undefined command is encountered. 

34DEFAULT_COMMAND = '' 

35 

36 

37 

38# ---------- UI notifier ---------- 

39 

40@enum.unique 

41class NotificationLevel(SortedEnum): 

42 INFO = 'info' 

43 ERROR = 'error' 

44 

45UiCallback: 'typing.TypeAlias' = 'Callable[[Message], None]' 

46 

47class Message: 

48 

49 ''' 

50 A message which should be displayed to the user. 

51 This is passed to the callback of the user interface which has been registered with :meth:`ConfigFile.set_ui_callback() <confattr.configfile.ConfigFile.set_ui_callback>`. 

52 

53 If you want full control how to display messages to the user you can access the attributes directly. 

54 Otherwise you can simply convert this object to a str, e.g. with ``str(msg)``. 

55 I recommend to use different colors for different values of :attr:`~confattr.configfile.Message.notification_level`. 

56 ''' 

57 

58 #: The value of :attr:`~confattr.configfile.Message.file_name` while loading environment variables. 

59 ENVIRONMENT_VARIABLES = 'environment variables' 

60 

61 

62 __slots__ = ('notification_level', 'message', 'file_name', 'line_number', 'line', 'no_context') 

63 

64 #: The importance of this message. I recommend to display messages of different importance levels in different colors. 

65 #: :class:`~confattr.configfile.ConfigFile` does not output messages which are less important than the :paramref:`~confattr.configfile.ConfigFile.notification_level` setting which has been passed to it's constructor. 

66 notification_level: NotificationLevel 

67 

68 #: The string or exception which should be displayed to the user 

69 message: 'str|BaseException' 

70 

71 #: The name of the config file which has caused this message. 

72 #: If this equals :const:`~confattr.configfile.Message.ENVIRONMENT_VARIABLES` it is not a file but the message has occurred while reading the environment variables. 

73 #: This is None if :meth:`ConfigFile.parse_line() <confattr.configfile.ConfigFile.parse_line>` is called directly, e.g. when parsing the input from a command line. 

74 file_name: 'str|None' 

75 

76 #: The number of the line in the config file. This is None if :attr:`~confattr.configfile.Message.file_name` is not a file name. 

77 line_number: 'int|None' 

78 

79 #: The line where the message occurred. This is an empty str if there is no line, e.g. when loading environment variables. 

80 line: str 

81 

82 #: If true: don't show line and line number. 

83 no_context: bool 

84 

85 

86 _last_file_name: 'str|None' = None 

87 

88 @classmethod 

89 def reset(cls) -> None: 

90 ''' 

91 If you are using :meth:`~confattr.configfile.Message.format_file_name_msg_line` or :meth:`~confattr.configfile.Message.__str__` 

92 you must call this method when the widget showing the error messages is cleared. 

93 ''' 

94 cls._last_file_name = None 

95 

96 def __init__(self, notification_level: NotificationLevel, message: 'str|BaseException', file_name: 'str|None' = None, line_number: 'int|None' = None, line: 'str' = '', no_context: bool = False) -> None: 

97 self.notification_level = notification_level 

98 self.message = message 

99 self.file_name = file_name 

100 self.line_number = line_number 

101 self.line = line 

102 self.no_context = no_context 

103 

104 @property 

105 def lvl(self) -> NotificationLevel: 

106 ''' 

107 An abbreviation for :attr:`~confattr.configfile.Message.notification_level` 

108 ''' 

109 return self.notification_level 

110 

111 def format_msg_line(self) -> str: 

112 ''' 

113 The return value includes the attributes :attr:`~confattr.configfile.Message.message`, :attr:`~confattr.configfile.Message.line_number` and :attr:`~confattr.configfile.Message.line` if they are set. 

114 ''' 

115 msg = str(self.message) 

116 if self.line and not self.no_context: 

117 if self.line_number is not None: 

118 lnref = 'line %s' % self.line_number 

119 else: 

120 lnref = 'line' 

121 return f'{msg} in {lnref} {self.line!r}' 

122 

123 return msg 

124 

125 def format_file_name(self) -> str: 

126 ''' 

127 :return: A header including the :attr:`~confattr.configfile.Message.file_name` if the :attr:`~confattr.configfile.Message.file_name` is different from the last time this function has been called or an empty string otherwise 

128 ''' 

129 file_name = '' if self.file_name is None else self.file_name 

130 if file_name == self._last_file_name: 

131 return '' 

132 

133 if file_name: 

134 out = f'While loading {file_name}:\n' 

135 else: 

136 out = '' 

137 

138 if self._last_file_name is not None: 

139 out = '\n' + out 

140 

141 type(self)._last_file_name = file_name 

142 

143 return out 

144 

145 

146 def format_file_name_msg_line(self) -> str: 

147 ''' 

148 :return: The concatenation of the return values of :meth:`~confattr.configfile.Message.format_file_name` and :meth:`~confattr.configfile.Message.format_msg_line` 

149 ''' 

150 return self.format_file_name() + self.format_msg_line() 

151 

152 

153 def __str__(self) -> str: 

154 ''' 

155 :return: The return value of :meth:`~confattr.configfile.Message.format_file_name_msg_line` 

156 ''' 

157 return self.format_file_name_msg_line() 

158 

159 def __repr__(self) -> str: 

160 return f'{type(self).__name__}(%s)' % ', '.join(f'{a}={self._format_attribute(getattr(self, a))}' for a in self.__slots__) 

161 

162 @staticmethod 

163 def _format_attribute(obj: object) -> str: 

164 if isinstance(obj, enum.Enum): 

165 return obj.name 

166 return repr(obj) 

167 

168 

169class UiNotifier: 

170 

171 ''' 

172 Most likely you will want to load the config file before creating the UI (user interface). 

173 But if there are errors in the config file the user will want to know about them. 

174 This class takes the messages from :class:`~confattr.configfile.ConfigFile` and stores them until the UI is ready. 

175 When you call :meth:`~confattr.configfile.UiNotifier.set_ui_callback` the stored messages will be forwarded and cleared. 

176 

177 This object can also filter the messages. 

178 :class:`~confattr.configfile.ConfigFile` calls :meth:`~confattr.configfile.UiNotifier.show_info` every time a setting is changed. 

179 If you load an entire config file this can be many messages and the user probably does not want to see them all. 

180 Therefore this object drops all messages of :const:`NotificationLevel.INFO <confattr.configfile.NotificationLevel.INFO>` by default. 

181 Pass :paramref:`~confattr.configfile.UiNotifier.notification_level` to the constructor if you don't want that. 

182 ''' 

183 

184 # ------- public methods ------- 

185 

186 def __init__(self, config_file: 'ConfigFile', notification_level: 'Config[NotificationLevel]|NotificationLevel' = NotificationLevel.ERROR) -> None: 

187 ''' 

188 :param config_file: Is used to add context information to messages, to which file and to which line a message belongs. 

189 :param notification_level: Messages which are less important than this notification level will be ignored. I recommend to pass a :class:`~confattr.config.Config` instance so that users can decide themselves what they want to see. 

190 ''' 

191 self._messages: 'list[Message]' = [] 

192 self._callback: 'UiCallback|None' = None 

193 self._notification_level = notification_level 

194 self._config_file = config_file 

195 

196 def set_ui_callback(self, callback: UiCallback) -> None: 

197 ''' 

198 Call :paramref:`~confattr.configfile.UiNotifier.set_ui_callback.callback` for all messages which have been saved by :meth:`~confattr.configfile.UiNotifier.show` and clear all saved messages afterwards. 

199 Save :paramref:`~confattr.configfile.UiNotifier.set_ui_callback.callback` for :meth:`~confattr.configfile.UiNotifier.show` to call. 

200 ''' 

201 self._callback = callback 

202 

203 for msg in self._messages: 

204 callback(msg) 

205 self._messages.clear() 

206 

207 

208 @property 

209 def notification_level(self) -> NotificationLevel: 

210 ''' 

211 Ignore messages that are less important than this level. 

212 ''' 

213 if isinstance(self._notification_level, Config): 

214 return self._notification_level.value 

215 else: 

216 return self._notification_level 

217 

218 @notification_level.setter 

219 def notification_level(self, val: NotificationLevel) -> None: 

220 if isinstance(self._notification_level, Config): 

221 self._notification_level.value = val 

222 else: 

223 self._notification_level = val 

224 

225 

226 # ------- called by ConfigFile ------- 

227 

228 def show_info(self, msg: str, *, ignore_filter: bool = False) -> None: 

229 ''' 

230 Call :meth:`~confattr.configfile.UiNotifier.show` with :const:`NotificationLevel.INFO <confattr.configfile.NotificationLevel.INFO>`. 

231 ''' 

232 self.show(NotificationLevel.INFO, msg, ignore_filter=ignore_filter) 

233 

234 def show_error(self, msg: 'str|BaseException', *, ignore_filter: bool = False) -> None: 

235 ''' 

236 Call :meth:`~confattr.configfile.UiNotifier.show` with :const:`NotificationLevel.ERROR <confattr.configfile.NotificationLevel.ERROR>`. 

237 ''' 

238 self.show(NotificationLevel.ERROR, msg, ignore_filter=ignore_filter) 

239 

240 

241 # ------- internal methods ------- 

242 

243 def show(self, notification_level: NotificationLevel, msg: 'str|BaseException', *, ignore_filter: bool = False, no_context: bool = False) -> None: 

244 ''' 

245 If a callback for the user interface has been registered with :meth:`~confattr.configfile.UiNotifier.set_ui_callback` call that callback. 

246 Otherwise save the message so that :meth:`~confattr.configfile.UiNotifier.set_ui_callback` can forward the message when :meth:`~confattr.configfile.UiNotifier.set_ui_callback` is called. 

247 

248 :param notification_level: The importance of the message 

249 :param msg: The message to be displayed on the user interface 

250 :param ignore_filter: If true: Show the message even if :paramref:`~confattr.configfile.UiNotifier.show.notification_level` is smaller then the :paramref:`UiNotifier.notification_level <confattr.configfile.UiNotifier.notification_level>`. 

251 :param no_context: If true: don't show line and line number. 

252 ''' 

253 if notification_level < self.notification_level and not ignore_filter: 

254 return 

255 

256 if not self._config_file.context_line_number and not self._config_file.show_line_always: 

257 no_context = True 

258 

259 message = Message( 

260 notification_level = notification_level, 

261 message = msg, 

262 file_name = self._config_file.context_file_name, 

263 line_number = self._config_file.context_line_number, 

264 line = self._config_file.context_line, 

265 no_context = no_context, 

266 ) 

267 

268 if self._callback: 

269 self._callback(message) 

270 else: 

271 self._messages.append(message) 

272 

273 

274# ---------- format help ---------- 

275 

276class SectionLevel(SortedEnum): 

277 

278 #: Is used to separate different commands in :meth:`ConfigFile.write_help() <confattr.configfile.ConfigFile.write_help>` and :meth:`ConfigFileCommand.save() <confattr.configfile.ConfigFileCommand.save>` 

279 SECTION = 'section' 

280 

281 #: Is used for subsections in :meth:`ConfigFileCommand.save() <confattr.configfile.ConfigFileCommand.save>` such as the "data types" section in the help of the set command 

282 SUB_SECTION = 'sub-section' 

283 

284 

285class FormattedWriter(abc.ABC): 

286 

287 @abc.abstractmethod 

288 def write_line(self, line: str) -> None: 

289 ''' 

290 Write a single line of documentation. 

291 :paramref:`~confattr.configfile.FormattedWriter.write_line.line` may *not* contain a newline. 

292 If :paramref:`~confattr.configfile.FormattedWriter.write_line.line` is empty it does not need to be prefixed with a comment character. 

293 Empty lines should be dropped if no other lines have been written before. 

294 ''' 

295 pass 

296 

297 def write_lines(self, text: str) -> None: 

298 ''' 

299 Write one or more lines of documentation. 

300 ''' 

301 for ln in text.splitlines(): 

302 self.write_line(ln) 

303 

304 @abc.abstractmethod 

305 def write_heading(self, lvl: SectionLevel, heading: str) -> None: 

306 ''' 

307 Write a heading. 

308 

309 This object should *not* add an indentation depending on the section 

310 because if the indentation is increased the line width should be decreased 

311 in order to keep the line wrapping consistent. 

312 Wrapping lines is handled by :class:`confattr.utils.HelpFormatter`, 

313 i.e. before the text is passed to this object. 

314 It would be possible to use :class:`argparse.RawTextHelpFormatter` instead 

315 and handle line wrapping on a higher level but that would require 

316 to understand the help generated by argparse 

317 in order to know how far to indent a broken line. 

318 One of the trickiest parts would probably be to get the indentation of the usage right. 

319 Keep in mind that the term "usage" can differ depending on the language settings of the user. 

320 

321 :param lvl: How to format the heading 

322 :param heading: The heading 

323 ''' 

324 pass 

325 

326 @abc.abstractmethod 

327 def write_command(self, cmd: str) -> None: 

328 ''' 

329 Write a config file command. 

330 ''' 

331 pass 

332 

333 

334class TextIOWriter(FormattedWriter): 

335 

336 def __init__(self, f: 'typing.TextIO|None') -> None: 

337 self.f = f 

338 self.ignore_empty_lines = True 

339 

340 def write_line_raw(self, line: str) -> None: 

341 if self.ignore_empty_lines and not line: 

342 return 

343 

344 print(line, file=self.f) 

345 self.ignore_empty_lines = False 

346 

347 

348class ConfigFileWriter(TextIOWriter): 

349 

350 def __init__(self, f: 'typing.TextIO|None', prefix: str) -> None: 

351 super().__init__(f) 

352 self.prefix = prefix 

353 

354 def write_command(self, cmd: str) -> None: 

355 self.write_line_raw(cmd) 

356 

357 def write_line(self, line: str) -> None: 

358 if line: 

359 line = self.prefix + line 

360 

361 self.write_line_raw(line) 

362 

363 def write_heading(self, lvl: SectionLevel, heading: str) -> None: 

364 if lvl is SectionLevel.SECTION: 

365 self.write_line('') 

366 self.write_line('') 

367 self.write_line('=' * len(heading)) 

368 self.write_line(heading) 

369 self.write_line('=' * len(heading)) 

370 else: 

371 self.write_line('') 

372 self.write_line(heading) 

373 self.write_line('-' * len(heading)) 

374 

375class HelpWriter(TextIOWriter): 

376 

377 def write_line(self, line: str) -> None: 

378 self.write_line_raw(line) 

379 

380 def write_heading(self, lvl: SectionLevel, heading: str) -> None: 

381 self.write_line('') 

382 if lvl is SectionLevel.SECTION: 

383 self.write_line(heading) 

384 self.write_line('=' * len(heading)) 

385 else: 

386 self.write_line(heading) 

387 self.write_line('-' * len(heading)) 

388 

389 def write_command(self, cmd: str) -> None: 

390 pass # pragma: no cover 

391 

392 

393# ---------- internal exceptions ---------- 

394 

395class ParseException(Exception): 

396 

397 ''' 

398 This is raised by :class:`~confattr.configfile.ConfigFileCommand` implementations and functions passed to :paramref:`~confattr.configfile.ConfigFile.check_config_id` in order to communicate an error in the config file like invalid syntax or an invalid value. 

399 Is caught in :class:`~confattr.configfile.ConfigFile`. 

400 ''' 

401 

402class MultipleParseExceptions(Exception): 

403 

404 ''' 

405 This is raised by :class:`~confattr.configfile.ConfigFileCommand` implementations in order to communicate that multiple errors have occured on the same line. 

406 Is caught in :class:`~confattr.configfile.ConfigFile`. 

407 ''' 

408 

409 def __init__(self, exceptions: 'Sequence[ParseException]') -> None: 

410 super().__init__() 

411 self.exceptions = exceptions 

412 

413 def __iter__(self) -> 'Iterator[ParseException]': 

414 return iter(self.exceptions) 

415 

416 

417# ---------- data types for **kw args ---------- 

418 

419if hasattr(typing, 'TypedDict'): # python >= 3.8 # pragma: no cover. This is tested but in a different environment which is not known to coverage. 

420 class SaveKwargs(typing.TypedDict, total=False): 

421 config_instances: 'Iterable[Config[typing.Any] | DictConfig[typing.Any, typing.Any]]' 

422 ignore: 'Iterable[Config[typing.Any] | DictConfig[typing.Any, typing.Any]] | None' 

423 no_multi: bool 

424 comments: bool 

425 

426 

427# ---------- ConfigFile class ---------- 

428 

429class ArgPos: 

430 ''' 

431 This is an internal class, the return type of :meth:`ConfigFile.find_arg() <confattr.configfile.ConfigFile.find_arg>` 

432 ''' 

433 

434 #: The index of the argument in :paramref:`~confattr.configfile.ConfigFile.find_arg.ln_split` where the cursor is located and which shall be completed. Please note that this can be one bigger than :paramref:`~confattr.configfile.ConfigFile.find_arg.ln_split` is long if the line ends on a space or a comment and the cursor is behind/in that space/comment. In that case :attr:`~confattr.configfile.ArgPos.in_between` is true. 

435 argument_pos: int 

436 

437 #: If true: The cursor is between two arguments, before the first argument or after the last argument. :attr:`~confattr.configfile.ArgPos.argument_pos` refers to the next argument, :attr:`argument_pos-1 <confattr.configfile.ArgPos.argument_pos>` to the previous argument. :attr:`~confattr.configfile.ArgPos.i0` is the start of the next argument, :attr:`~confattr.configfile.ArgPos.i1` is the end of the previous argument. 

438 in_between: bool 

439 

440 #: The index in :paramref:`~confattr.configfile.ConfigFile.find_arg.line` where the argument having the cursor starts (inclusive) or the start of the next argument if :attr:`~confattr.configfile.ArgPos.in_between` is true 

441 i0: int 

442 

443 #: The index in :paramref:`~confattr.configfile.ConfigFile.find_arg.line` where the current word ends (exclusive) or the end of the previous argument if :attr:`~confattr.configfile.ArgPos.in_between` is true 

444 i1: int 

445 

446 

447class ConfigFile: 

448 

449 ''' 

450 Read or write a config file. 

451 ''' 

452 

453 COMMENT = '#' 

454 COMMENT_PREFIXES = ('"', '#') 

455 ENTER_GROUP_PREFIX = '[' 

456 ENTER_GROUP_SUFFIX = ']' 

457 

458 #: How to separete several element in a collection (list, set, dict) 

459 ITEM_SEP = ',' 

460 

461 #: How to separate key and value in a dict 

462 KEY_SEP = ':' 

463 

464 

465 #: The :class:`~confattr.config.Config` instances to load or save 

466 config_instances: 'dict[str, Config[typing.Any]]' 

467 

468 #: While loading a config file: The group that is currently being parsed, i.e. an identifier for which object(s) the values shall be set. This is set in :meth:`~confattr.configfile.ConfigFile.enter_group` and reset in :meth:`~confattr.configfile.ConfigFile.load_file`. 

469 config_id: 'ConfigId|None' 

470 

471 #: Override the config file which is returned by :meth:`~confattr.configfile.ConfigFile.iter_config_paths`. 

472 #: You should set either this attribute or :attr:`~confattr.configfile.ConfigFile.config_directory` in your tests with :meth:`monkeypatch.setattr() <pytest.MonkeyPatch.setattr>`. 

473 #: If the environment variable ``APPNAME_CONFIG_PATH`` is set this attribute is set to it's value in the constructor (where ``APPNAME`` is the value which is passed as :paramref:`~confattr.configfile.ConfigFile.appname` to the constructor but in all upper case letters and hyphens and spaces replaced by underscores.) 

474 config_path: 'str|None' = None 

475 

476 #: Override the config directory which is returned by :meth:`~confattr.configfile.ConfigFile.iter_user_site_config_paths`. 

477 #: You should set either this attribute or :attr:`~confattr.configfile.ConfigFile.config_path` in your tests with :meth:`monkeypatch.setattr() <pytest.MonkeyPatch.setattr>`. 

478 #: If the environment variable ``APPNAME_CONFIG_DIRECTORY`` is set this attribute is set to it's value in the constructor (where ``APPNAME`` is the value which is passed as :paramref:`~confattr.configfile.ConfigFile.appname` to the constructor but in all upper case letters and hyphens and spaces replaced by underscores.) 

479 config_directory: 'str|None' = None 

480 

481 #: The name of the config file used by :meth:`~confattr.configfile.ConfigFile.iter_config_paths`. 

482 #: Can be changed with the environment variable ``APPNAME_CONFIG_NAME`` (where ``APPNAME`` is the value which is passed as :paramref:`~confattr.configfile.ConfigFile.appname` to the constructor but in all upper case letters and hyphens and spaces replaced by underscores.). 

483 config_name = 'config' 

484 

485 #: Contains the names of the environment variables for :attr:`~confattr.configfile.ConfigFile.config_path`, :attr:`~confattr.configfile.ConfigFile.config_directory` and :attr:`~confattr.configfile.ConfigFile.config_name`—in capital letters and prefixed with :attr:`~confattr.configfile.ConfigFile.envprefix`. 

486 env_variables: 'list[str]' 

487 

488 #: A prefix that is prepended to the name of environment variables in :meth:`~confattr.configfile.ConfigFile.get_env_name`. 

489 #: It is set in the constructor by first setting it to an empty str and then passing the value of :paramref:`~confattr.configfile.ConfigFile.appname` to :meth:`~confattr.configfile.ConfigFile.get_env_name` and appending an underscore. 

490 envprefix: str 

491 

492 #: The name of the file which is currently loaded. If this equals :attr:`Message.ENVIRONMENT_VARIABLES <confattr.configfile.Message.ENVIRONMENT_VARIABLES>` it is no file name but an indicator that environment variables are loaded. This is :obj:`None` if :meth:`~confattr.configfile.ConfigFile.parse_line` is called directly (e.g. the input from a command line is parsed). 

493 context_file_name: 'str|None' = None 

494 #: The number of the line which is currently parsed. This is :obj:`None` if :attr:`~confattr.configfile.ConfigFile.context_file_name` is not a file name. 

495 context_line_number: 'int|None' = None 

496 #: The line which is currently parsed. 

497 context_line: str = '' 

498 

499 #: If true: ``[config-id]`` syntax is allowed in config file, config ids are included in help, config id related options are available for include. 

500 #: If false: It is not possible to set different values for different objects (but default values for :class:`~confattr.config.MultiConfig` instances can be set) 

501 enable_config_ids: bool 

502 

503 

504 #: A mapping from the name to the object for all commands that are available in this config file. If a command has :attr:`~confattr.configfile.ConfigFileCommand.aliases` every alias appears in this mapping, too. Use :attr:`~confattr.configfile.ConfigFile.commands` instead if you want to iterate over all available commands. This is generated in the constructor based on :paramref:`~confattr.configfile.ConfigFile.commands` if it is given or based on the return value of :meth:`ConfigFileCommand.get_command_types() <confattr.configfile.ConfigFileCommand.get_command_types>` otherwise. Note that you are passing a sequence of *types* as argument but this attribute contains the instantiated *objects*. 

505 command_dict: 'dict[str, ConfigFileCommand]' 

506 

507 #: A list of all commands that are available in this config file. This is generated in the constructor based on :paramref:`~confattr.configfile.ConfigFile.commands` if it is given or based on the return value of :meth:`ConfigFileCommand.get_command_types() <confattr.configfile.ConfigFileCommand.get_command_types>` otherwise. Note that you are passing a sequence of *types* as argument but this attribute contains the instantiated *objects*. In contrast to :attr:`~confattr.configfile.ConfigFile.command_dict` this list contains every command only once. 

508 commands: 'list[ConfigFileCommand]' 

509 

510 

511 #: See :paramref:`~confattr.configfile.ConfigFile.check_config_id` 

512 check_config_id: 'Callable[[ConfigId], None]|None' 

513 

514 #: If this is true :meth:`ui_notifier.show() <confattr.configfile.UiNotifier.show>` concatenates :attr:`~confattr.configfile.ConfigFile.context_line` to the message even if :attr:`~confattr.configfile.ConfigFile.context_line_number` is not set. 

515 show_line_always: bool 

516 

517 

518 def __init__(self, *, 

519 notification_level: 'Config[NotificationLevel]' = NotificationLevel.ERROR, # type: ignore [assignment] # yes, passing a NotificationLevel directly is possible but I don't want users to do that in order to give the users of their applications the freedom to set this the way they need it 

520 appname: str, 

521 authorname: 'str|None' = None, 

522 config_instances: 'dict[str, Config[typing.Any]]' = Config.instances, 

523 commands: 'Sequence[type[ConfigFileCommand]]|None' = None, 

524 formatter_class: 'type[argparse.HelpFormatter]' = HelpFormatter, 

525 check_config_id: 'Callable[[ConfigId], None]|None' = None, 

526 enable_config_ids: 'bool|None' = None, 

527 show_line_always: bool = True, 

528 ) -> None: 

529 ''' 

530 :param notification_level: A :class:`~confattr.config.Config` which the users of your application can set to choose whether they want to see information which might be interesting for debugging a config file. A :class:`~confattr.configfile.Message` with a priority lower than this value is *not* passed to the callback registered with :meth:`~confattr.configfile.ConfigFile.set_ui_callback`. 

531 :param appname: The name of the application, required for generating the path of the config file if you use :meth:`~confattr.configfile.ConfigFile.load` or :meth:`~confattr.configfile.ConfigFile.save` and as prefix of environment variable names 

532 :param authorname: The name of the developer of the application, on MS Windows useful for generating the path of the config file if you use :meth:`~confattr.configfile.ConfigFile.load` or :meth:`~confattr.configfile.ConfigFile.save` 

533 :param config_instances: The Config instances to load or save, defaults to :attr:`Config.instances <confattr.config.Config.instances>` 

534 :param commands: The commands (as subclasses of :class:`~confattr.configfile.ConfigFileCommand` or :class:`~confattr.configfile.ConfigFileArgparseCommand`) allowed in this config file, if this is :obj:`None`: use the return value of :meth:`ConfigFileCommand.get_command_types() <confattr.configfile.ConfigFileCommand.get_command_types>` 

535 :param formatter_class: Is used to clean up doc strings and wrap lines in the help 

536 :param check_config_id: Is called every time a configuration group is opened (except for :attr:`Config.default_config_id <confattr.config.Config.default_config_id>`—that is always allowed). The callback should raise a :class:`~confattr.configfile.ParseException` if the config id is invalid. 

537 :param enable_config_ids: see :attr:`~confattr.configfile.ConfigFile.enable_config_ids`. If None: Choose True or False automatically based on :paramref:`~confattr.configfile.ConfigFile.check_config_id` and the existence of :class:`~confattr.config.MultiConfig`/:class:`~confattr.config.MultiDictConfig` 

538 :param show_line_always: If false: when calling :meth:`UiNotifier.show() <confattr.configfile.UiNotifier.show>` :attr:`~confattr.configfile.ConfigFile.context_line` and :attr:`~confattr.configfile.ConfigFile.context_line_number` are concatenated to the message if both are set. If :attr:`~confattr.configfile.ConfigFile.context_line_number` is not set it is assumed that the line comes from a command line interface where the user just entered it and it is still visible so there is no need to print it again. If :paramref:`~confattr.configfile.ConfigFile.show_line_always` is true (the default) :attr:`~confattr.configfile.ConfigFile.context_line` is concatenated even if :attr:`~confattr.configfile.ConfigFile.context_line_number` is not set. That is useful when you use :meth:`~confattr.configfile.ConfigFile.parse_line` to parse a command which has been assigned to a keyboard shortcut. 

539 ''' 

540 self.appname = appname 

541 self.authorname = authorname 

542 self.ui_notifier = UiNotifier(self, notification_level) 

543 self.config_instances = config_instances 

544 self.config_id: 'ConfigId|None' = None 

545 self.formatter_class = formatter_class 

546 self.env_variables: 'list[str]' = [] 

547 self.check_config_id = check_config_id 

548 self.show_line_always = show_line_always 

549 

550 if enable_config_ids is None: 

551 enable_config_ids = self.check_config_id is not None or any(isinstance(cfg, MultiConfig) for cfg in self.config_instances.values()) 

552 self.enable_config_ids = enable_config_ids 

553 

554 self.envprefix = '' 

555 self.envprefix = self.get_env_name(appname + '_') 

556 envname = self.envprefix + 'CONFIG_PATH' 

557 self.env_variables.append(envname) 

558 if envname in os.environ: 

559 self.config_path = os.environ[envname] 

560 envname = self.envprefix + 'CONFIG_DIRECTORY' 

561 self.env_variables.append(envname) 

562 if envname in os.environ: 

563 self.config_directory = os.environ[envname] 

564 envname = self.envprefix + 'CONFIG_NAME' 

565 self.env_variables.append(envname) 

566 if envname in os.environ: 

567 self.config_name = os.environ[envname] 

568 

569 if commands is None: 

570 commands = ConfigFileCommand.get_command_types() 

571 self.command_dict = {} 

572 self.commands = [] 

573 for cmd_type in commands: 

574 cmd = cmd_type(self) 

575 self.commands.append(cmd) 

576 for name in cmd.get_names(): 

577 self.command_dict[name] = cmd 

578 

579 

580 def set_ui_callback(self, callback: UiCallback) -> None: 

581 ''' 

582 Register a callback to a user interface in order to show messages to the user like syntax errors or invalid values in the config file. 

583 

584 Messages which occur before this method is called are stored and forwarded as soon as the callback is registered. 

585 

586 :param ui_callback: A function to display messages to the user 

587 ''' 

588 self.ui_notifier.set_ui_callback(callback) 

589 

590 def get_app_dirs(self) -> 'appdirs.AppDirs': 

591 ''' 

592 Create or get a cached `AppDirs <https://github.com/ActiveState/appdirs/blob/master/README.rst#appdirs-for-convenience>`__ instance with multipath support enabled. 

593 

594 When creating a new instance, `platformdirs <https://pypi.org/project/platformdirs/>`__, `xdgappdirs <https://pypi.org/project/xdgappdirs/>`__ and `appdirs <https://pypi.org/project/appdirs/>`__ are tried, in that order. 

595 The first one installed is used. 

596 appdirs, the original of the two forks and the only one of the three with type stubs, is specified in pyproject.toml as a hard dependency so that at least one of the three should always be available. 

597 I am not very familiar with the differences but if a user finds that appdirs does not work for them they can choose to use an alternative with ``pipx inject appname xdgappdirs|platformdirs``. 

598 

599 These libraries should respect the environment variables ``XDG_CONFIG_HOME`` and ``XDG_CONFIG_DIRS``. 

600 ''' 

601 if not hasattr(self, '_appdirs'): 

602 try: 

603 import platformdirs # type: ignore [import] # this library is not typed and not necessarily installed, I am relying on it's compatibility with appdirs 

604 AppDirs = typing.cast('type[appdirs.AppDirs]', platformdirs.PlatformDirs) # pragma: no cover # This is tested but in a different tox environment 

605 except ImportError: 

606 try: 

607 import xdgappdirs # type: ignore [import] # this library is not typed and not necessarily installed, I am relying on it's compatibility with appdirs 

608 AppDirs = typing.cast('type[appdirs.AppDirs]', xdgappdirs.AppDirs) # pragma: no cover # This is tested but in a different tox environment 

609 except ImportError: 

610 AppDirs = appdirs.AppDirs 

611 

612 self._appdirs = AppDirs(self.appname, self.authorname, multipath=True) 

613 

614 return self._appdirs 

615 

616 # ------- load ------- 

617 

618 def iter_user_site_config_paths(self) -> 'Iterator[str]': 

619 ''' 

620 Iterate over all directories which are searched for config files, user specific first. 

621 

622 The directories are based on :meth:`~confattr.configfile.ConfigFile.get_app_dirs` 

623 unless :attr:`~confattr.configfile.ConfigFile.config_directory` has been set. 

624 If :attr:`~confattr.configfile.ConfigFile.config_directory` has been set 

625 it's value is yielded and nothing else. 

626 ''' 

627 if self.config_directory: 

628 yield self.config_directory 

629 return 

630 

631 appdirs = self.get_app_dirs() 

632 yield from appdirs.user_config_dir.split(os.path.pathsep) 

633 yield from appdirs.site_config_dir.split(os.path.pathsep) 

634 

635 def iter_config_paths(self) -> 'Iterator[str]': 

636 ''' 

637 Iterate over all paths which are checked for config files, user specific first. 

638 

639 Use this method if you want to tell the user where the application is looking for it's config file. 

640 The first existing file yielded by this method is used by :meth:`~confattr.configfile.ConfigFile.load`. 

641 

642 The paths are generated by joining the directories yielded by :meth:`~confattr.configfile.ConfigFile.iter_user_site_config_paths` with 

643 :attr:`ConfigFile.config_name <confattr.configfile.ConfigFile.config_name>`. 

644 

645 If :attr:`~confattr.configfile.ConfigFile.config_path` has been set this method yields that path instead and no other paths. 

646 ''' 

647 if self.config_path: 

648 yield self.config_path 

649 return 

650 

651 for path in self.iter_user_site_config_paths(): 

652 yield os.path.join(path, self.config_name) 

653 

654 def load(self, *, env: bool = True) -> None: 

655 ''' 

656 Load the first existing config file returned by :meth:`~confattr.configfile.ConfigFile.iter_config_paths`. 

657 

658 If there are several config files a user specific config file is preferred. 

659 If a user wants a system wide config file to be loaded, too, they can explicitly include it in their config file. 

660 :param env: If true: call :meth:`~confattr.configfile.ConfigFile.load_env` after loading the config file. 

661 ''' 

662 for fn in self.iter_config_paths(): 

663 if os.path.isfile(fn): 

664 self.load_file(fn) 

665 break 

666 

667 if env: 

668 self.load_env() 

669 

670 def load_env(self) -> None: 

671 ''' 

672 Load settings from environment variables. 

673 The name of the environment variable belonging to a setting is generated with :meth:`~confattr.configfile.ConfigFile.get_env_name`. 

674 

675 Environment variables not matching a setting or having an invalid value are reported with :meth:`self.ui_notifier.show_error() <confattr.configfile.UiNotifier.show_error>`. 

676 

677 :raises ValueError: if two settings have the same environment variable name (see :meth:`~confattr.configfile.ConfigFile.get_env_name`) or the environment variable name for a setting collides with one of the standard environment variables listed in :attr:`~confattr.configfile.ConfigFile.env_variables` 

678 ''' 

679 old_file_name = self.context_file_name 

680 self.context_file_name = Message.ENVIRONMENT_VARIABLES 

681 

682 config_instances: 'dict[str, Config[object]]' = {} 

683 for key, instance in self.config_instances.items(): 

684 name = self.get_env_name(key) 

685 if name in self.env_variables: 

686 raise ValueError(f'setting {instance.key!r} conflicts with environment variable {name!r}') 

687 elif name in config_instances: 

688 raise ValueError(f'settings {instance.key!r} and {config_instances[name].key!r} result in the same environment variable {name!r}') 

689 else: 

690 config_instances[name] = instance 

691 

692 for name, value in os.environ.items(): 

693 if not name.startswith(self.envprefix): 

694 continue 

695 if name in self.env_variables: 

696 continue 

697 

698 if name in config_instances: 

699 instance = config_instances[name] 

700 try: 

701 instance.set_value(config_id=None, value=self.parse_value(instance, value, raw=True)) 

702 self.ui_notifier.show_info(f'set {instance.key} to {self.format_value(instance, config_id=None)}') 

703 except ValueError as e: 

704 self.ui_notifier.show_error(f"{e} while trying to parse environment variable {name}='{value}'") 

705 else: 

706 self.ui_notifier.show_error(f"unknown environment variable {name}='{value}'") 

707 

708 self.context_file_name = old_file_name 

709 

710 

711 def get_env_name(self, key: str) -> str: 

712 ''' 

713 Convert the key of a setting to the name of the corresponding environment variable. 

714 

715 :return: An all upper case version of :paramref:`~confattr.configfile.ConfigFile.get_env_name.key` with all hyphens, dots and spaces replaced by underscores and :attr:`~confattr.configfile.ConfigFile.envprefix` prepended to the result. 

716 ''' 

717 out = key 

718 out = out.upper() 

719 for c in ' .-': 

720 out = out.replace(c, '_') 

721 out = self.envprefix + out 

722 return out 

723 

724 def load_file(self, fn: str) -> None: 

725 ''' 

726 Load a config file and change the :class:`~confattr.config.Config` objects accordingly. 

727 

728 Use :meth:`~confattr.configfile.ConfigFile.set_ui_callback` to get error messages which appeared while loading the config file. 

729 You can call :meth:`~confattr.configfile.ConfigFile.set_ui_callback` after this method without loosing any messages. 

730 

731 :param fn: The file name of the config file (absolute or relative path) 

732 ''' 

733 self.config_id = None 

734 self.load_without_resetting_config_id(fn) 

735 

736 def load_without_resetting_config_id(self, fn: str) -> None: 

737 old_file_name = self.context_file_name 

738 self.context_file_name = fn 

739 

740 with open(fn, 'rt') as f: 

741 for lnno, ln in enumerate(f, 1): 

742 self.context_line_number = lnno 

743 self.parse_line(line=ln) 

744 self.context_line_number = None 

745 

746 self.context_file_name = old_file_name 

747 

748 def parse_line(self, line: str) -> bool: 

749 ''' 

750 :param line: The line to be parsed 

751 :return: True if line is valid, False if an error has occurred 

752 

753 :meth:`~confattr.configfile.ConfigFile.parse_error` is called if something goes wrong (i.e. if the return value is False), e.g. invalid key or invalid value. 

754 ''' 

755 ln = line.strip() 

756 if not ln: 

757 return True 

758 if self.is_comment(ln): 

759 return True 

760 if self.enable_config_ids and self.enter_group(ln): 

761 return True 

762 

763 self.context_line = ln 

764 

765 try: 

766 ln_split = self.split_line(ln) 

767 except Exception as e: 

768 self.parse_error(str(e)) 

769 out = False 

770 else: 

771 out = self.parse_split_line(ln_split) 

772 

773 self.context_line = '' 

774 return out 

775 

776 def split_line(self, line: str) -> 'list[str]': 

777 cmd, line = self.split_one_symbol_command(line) 

778 line_split = shlex.split(line, comments=True) 

779 if cmd: 

780 line_split.insert(0, cmd) 

781 return line_split 

782 

783 def split_line_ignore_errors(self, line: str) -> 'list[str]': 

784 out = [] 

785 cmd, line = self.split_one_symbol_command(line) 

786 if cmd: 

787 out.append(cmd) 

788 lex = shlex.shlex(line, posix=True) 

789 lex.whitespace_split = True 

790 while True: 

791 try: 

792 t = lex.get_token() 

793 except: 

794 out.append(lex.token) 

795 return out 

796 if t is None: 

797 return out 

798 out.append(t) 

799 

800 def split_one_symbol_command(self, line: str) -> 'tuple[str|None, str]': 

801 if line and not line[0].isalnum() and line[0] in self.command_dict: 

802 return line[0], line[1:] 

803 

804 return None, line 

805 

806 

807 def is_comment(self, line: str) -> bool: 

808 ''' 

809 Check if :paramref:`~confattr.configfile.ConfigFile.is_comment.line` is a comment. 

810 

811 :param line: The current line 

812 :return: :obj:`True` if :paramref:`~confattr.configfile.ConfigFile.is_comment.line` is a comment 

813 ''' 

814 for c in self.COMMENT_PREFIXES: 

815 if line.startswith(c): 

816 return True 

817 return False 

818 

819 def enter_group(self, line: str) -> bool: 

820 ''' 

821 Check if :paramref:`~confattr.configfile.ConfigFile.enter_group.line` starts a new group and set :attr:`~confattr.configfile.ConfigFile.config_id` if it does. 

822 Call :meth:`~confattr.configfile.ConfigFile.parse_error` if :meth:`~confattr.configfile.ConfigFile.check_config_id` raises a :class:`~confattr.configfile.ParseException`. 

823 

824 :param line: The current line 

825 :return: :obj:`True` if :paramref:`~confattr.configfile.ConfigFile.enter_group.line` starts a new group 

826 ''' 

827 if line.startswith(self.ENTER_GROUP_PREFIX) and line.endswith(self.ENTER_GROUP_SUFFIX): 

828 config_id = typing.cast(ConfigId, line[len(self.ENTER_GROUP_PREFIX):-len(self.ENTER_GROUP_SUFFIX)]) 

829 if self.check_config_id and config_id != Config.default_config_id: 

830 try: 

831 self.check_config_id(config_id) 

832 except ParseException as e: 

833 self.parse_error(str(e)) 

834 self.config_id = config_id 

835 if self.config_id not in MultiConfig.config_ids: 

836 MultiConfig.config_ids.append(self.config_id) 

837 return True 

838 return False 

839 

840 def parse_split_line(self, ln_split: 'Sequence[str]') -> bool: 

841 ''' 

842 Call the corresponding command in :attr:`~confattr.configfile.ConfigFile.command_dict`. 

843 If any :class:`~confattr.configfile.ParseException` or :class:`~confattr.configfile.MultipleParseExceptions` is raised catch it and call :meth:`~confattr.configfile.ConfigFile.parse_error`. 

844 

845 :return: False if a :class:`~confattr.configfile.ParseException` or :class:`~confattr.configfile.MultipleParseExceptions` has been caught, True if no exception has been caught 

846 ''' 

847 cmd = self.get_command(ln_split) 

848 try: 

849 cmd.run(ln_split) 

850 except ParseException as e: 

851 self.parse_error(str(e)) 

852 return False 

853 except MultipleParseExceptions as exceptions: 

854 for exc in exceptions: 

855 self.parse_error(str(exc)) 

856 return False 

857 

858 return True 

859 

860 def get_command(self, ln_split: 'Sequence[str]') -> 'ConfigFileCommand': 

861 cmd_name = ln_split[0] 

862 if cmd_name in self.command_dict: 

863 cmd = self.command_dict[cmd_name] 

864 elif DEFAULT_COMMAND in self.command_dict: 

865 cmd = self.command_dict[DEFAULT_COMMAND] 

866 else: 

867 cmd = UnknownCommand(self) 

868 return cmd 

869 

870 

871 # ------- save ------- 

872 

873 def get_save_path(self) -> str: 

874 ''' 

875 :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. 

876 ''' 

877 paths = tuple(self.iter_config_paths()) 

878 for fn in paths: 

879 if os.path.isfile(fn) and os.access(fn, os.W_OK): 

880 return fn 

881 

882 return paths[0] 

883 

884 def save(self, 

885 if_not_existing: bool = False, 

886 **kw: 'Unpack[SaveKwargs]', 

887 ) -> str: 

888 ''' 

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

890 Directories are created as necessary. 

891 

892 :param config_instances: Do not save all settings but only those given. If this is a :class:`list` they are written in the given order. If this is a :class:`set` they are sorted by their keys. 

893 :param ignore: Do not write these settings to the file. 

894 :param no_multi: Do not write several sections. For :class:`~confattr.config.MultiConfig` instances write the default values only. 

895 :param comments: Write comments with allowed values and help. 

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

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

898 ''' 

899 fn = self.get_save_path() 

900 if if_not_existing and os.path.isfile(fn): 

901 return fn 

902 

903 # "If, when attempting to write a file, the destination directory is non-existent an attempt should be made to create it with permission 0700. 

904 # If the destination directory exists already the permissions should not be changed." 

905 # https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html 

906 os.makedirs(os.path.dirname(fn), exist_ok=True, mode=0o0700) 

907 self.save_file(fn, **kw) 

908 return fn 

909 

910 def save_file(self, 

911 fn: str, 

912 **kw: 'Unpack[SaveKwargs]' 

913 ) -> None: 

914 ''' 

915 Save the current values of all settings to a specific file. 

916 

917 :param fn: The name of the file to write to. If this is not an absolute path it is relative to the current working directory. 

918 :raises FileNotFoundError: if the directory does not exist 

919 

920 For an explanation of the other parameters see :meth:`~confattr.configfile.ConfigFile.save`. 

921 ''' 

922 with open(fn, 'wt') as f: 

923 self.save_to_open_file(f, **kw) 

924 

925 

926 def save_to_open_file(self, 

927 f: typing.TextIO, 

928 **kw: 'Unpack[SaveKwargs]', 

929 ) -> None: 

930 ''' 

931 Save the current values of all settings to a file-like object 

932 by creating a :class:`~confattr.configfile.ConfigFileWriter` object and calling :meth:`~confattr.configfile.ConfigFile.save_to_writer`. 

933 

934 :param f: The file to write to 

935 

936 For an explanation of the other parameters see :meth:`~confattr.configfile.ConfigFile.save`. 

937 ''' 

938 writer = ConfigFileWriter(f, prefix=self.COMMENT + ' ') 

939 self.save_to_writer(writer, **kw) 

940 

941 def save_to_writer(self, writer: FormattedWriter, **kw: 'Unpack[SaveKwargs]') -> None: 

942 ''' 

943 Save the current values of all settings. 

944 

945 Ensure that all keyword arguments are passed with :meth:`~confattr.configfile.ConfigFile.set_save_default_arguments`. 

946 Iterate over all :class:`~confattr.configfile.ConfigFileCommand` objects in :attr:`~confattr.configfile.ConfigFile.commands` and do for each of them: 

947 

948 - set :attr:`~confattr.configfile.ConfigFileCommand.should_write_heading` to :obj:`True` if :python:`getattr(cmd.save, 'implemented', True)` is true for two or more of those commands or to :obj:`False` otherwise 

949 - call :meth:`~confattr.configfile.ConfigFileCommand.save` 

950 ''' 

951 self.set_save_default_arguments(kw) 

952 commands = self.commands 

953 write_headings = len(tuple(cmd for cmd in commands if getattr(cmd.save, 'implemented', True))) >= 2 

954 for cmd in commands: 

955 cmd.should_write_heading = write_headings 

956 cmd.save(writer, **kw) 

957 

958 def set_save_default_arguments(self, kw: 'SaveKwargs') -> None: 

959 ''' 

960 Ensure that all arguments are given in :paramref:`~confattr.configfile.ConfigFile.set_save_default_arguments.kw`. 

961 ''' 

962 kw.setdefault('config_instances', set(self.config_instances.values())) 

963 kw.setdefault('ignore', None) 

964 kw.setdefault('no_multi', not self.enable_config_ids) 

965 kw.setdefault('comments', True) 

966 

967 

968 def quote(self, val: str) -> str: 

969 ''' 

970 Quote a value if necessary so that it will be interpreted as one argument. 

971 

972 The default implementation calls :func:`~confattr.utils.readable_quote`. 

973 ''' 

974 return readable_quote(val) 

975 

976 def write_config_id(self, writer: FormattedWriter, config_id: ConfigId) -> None: 

977 ''' 

978 Start a new group in the config file so that all following commands refer to the given :paramref:`~confattr.configfile.ConfigFile.write_config_id.config_id`. 

979 ''' 

980 writer.write_command(self.ENTER_GROUP_PREFIX + config_id + self.ENTER_GROUP_SUFFIX) 

981 

982 def get_help_config_id(self) -> str: 

983 ''' 

984 :return: A help how to use :class:`~confattr.config.MultiConfig`. The return value still needs to be cleaned with :func:`inspect.cleandoc`. 

985 ''' 

986 return f''' 

987 You can specify the object that a value shall refer to by inserting the line `{self.ENTER_GROUP_PREFIX}config-id{self.ENTER_GROUP_SUFFIX}` above. 

988 `config-id` must be replaced by the corresponding identifier for the object. 

989 ''' 

990 

991 

992 # ------- formatting and parsing of values ------- 

993 

994 def format_value(self, instance: Config[typing.Any], config_id: 'ConfigId|None') -> str: 

995 ''' 

996 :param instance: The config value to be saved 

997 :param config_id: Which value to be written in case of a :class:`~confattr.config.MultiConfig`, should be :obj:`None` for a normal :class:`~confattr.config.Config` instance 

998 :return: A str representation to be written to the config file 

999 

1000 Convert the value of the :class:`~confattr.config.Config` instance into a str with :meth:`~confattr.configfile.ConfigFile.format_any_value`. 

1001 ''' 

1002 return self.format_any_value(instance.type, instance.get_value(config_id)) 

1003 

1004 def format_any_value(self, type: 'AbstractFormatter[T2]', value: 'T2') -> str: 

1005 return type.format_value(self, value) 

1006 

1007 

1008 def parse_value(self, instance: 'Config[T2]', value: str, *, raw: bool) -> 'T2': 

1009 ''' 

1010 :param instance: The config instance for which the value should be parsed, this is important for the data type 

1011 :param value: The string representation of the value to be parsed 

1012 :param raw: if false: expand :paramref:`~confattr.configfile.ConfigFile.parse_value.value` with :meth:`~confattr.configfile.ConfigFile.expand` first, if true: parse :paramref:`~confattr.configfile.ConfigFile.parse_value.value` as it is 

1013 Parse a value to the data type of a given setting by calling :meth:`~confattr.configfile.ConfigFile.parse_value_part` 

1014 ''' 

1015 if not raw: 

1016 value = self.expand(value) 

1017 return self.parse_value_part(instance, instance.type, value) 

1018 

1019 def parse_value_part(self, config: 'Config[typing.Any]', t: 'AbstractFormatter[T2]', value: str) -> 'T2': 

1020 ''' 

1021 Parse a value to the given data type. 

1022 

1023 :param config: Needed for the allowed values and the key for error messages 

1024 :param t: The data type to which :paramref:`~confattr.configfile.ConfigFile.parse_value_part.value` shall be parsed 

1025 :param value: The value to be parsed 

1026 :raises ValueError: if :paramref:`~confattr.configfile.ConfigFile.parse_value_part.value` is invalid 

1027 ''' 

1028 return t.parse_value(self, value) 

1029 

1030 

1031 def expand(self, arg: str) -> str: 

1032 return self.expand_config(self.expand_env(arg)) 

1033 

1034 reo_config = re.compile(r'%([^%]*)%') 

1035 def expand_config(self, arg: str) -> str: 

1036 n = arg.count('%') 

1037 if n % 2 == 1: 

1038 raise ParseException("uneven number of percent characters, use %% for a literal percent sign or --raw if you don't want expansion") 

1039 return self.reo_config.sub(self.expand_config_match, arg) 

1040 

1041 reo_env = re.compile(r'\$\{([^{}]*)\}') 

1042 def expand_env(self, arg: str) -> str: 

1043 return self.reo_env.sub(self.expand_env_match, arg) 

1044 

1045 def expand_config_match(self, m: 're.Match[str]') -> str: 

1046 ''' 

1047 :param m: A match of :attr:`~confattr.configfile.ConfigFile.reo_config`, group 1 is the :attr:`Config.key <confattr.config.Config.key>` possibly including a ``!conversion`` or a ``:format_spec`` 

1048 :return: The expanded form of the setting or ``'%'`` if group 1 is empty 

1049 :raises ParseException: If ``key``, ``!conversion`` or ``:format_spec`` is invalid 

1050 

1051 This is based on the `Python Format String Syntax <https://docs.python.org/3/library/string.html#format-string-syntax>`__. 

1052 

1053 ``field_name`` is the :attr:`~confattr.config.Config.key`. 

1054 

1055 ``!conversion`` is one of: 

1056 

1057 - ``!``: :meth:`ConfigFile.format_value() <confattr.configfile.ConfigFile.format_value>` 

1058 - ``!r``: :func:`repr` 

1059 - ``!s``: :class:`str` 

1060 - ``!a``: :func:`ascii` 

1061 

1062 ``:format_spec`` depends on the :attr:`Config.type <confattr.config.Config.type>`, see the `Python Format Specification Mini-Language <https://docs.python.org/3/library/string.html#formatspec>`__. 

1063 :meth:`List() <confattr.formatters.List.expand_value>`, :meth:`Set() <confattr.formatters.Set.expand_value>` and :meth:`Dict() <confattr.formatters.Dict.expand_value>` implement :meth:`~confattr.formatters.AbstractFormatter.expand_value` so that you can access specific items. 

1064 If :meth:`~confattr.formatters.AbstractFormatter.expand_value` raises an :class:`Exception` it is caught and reraised as a :class:`~confattr.configfile.ParseException`. 

1065 ''' 

1066 key = m.group(1) 

1067 if not key: 

1068 return '%' 

1069 

1070 if ':' in key: 

1071 key, fmt = key.split(':', 1) 

1072 else: 

1073 fmt = None 

1074 if '!' in key: 

1075 key, stringifier = key.split('!', 1) 

1076 else: 

1077 stringifier = None 

1078 

1079 if key not in self.config_instances: 

1080 raise ParseException(f'invalid key {key!r}') 

1081 instance = self.config_instances[key] 

1082 

1083 if stringifier is None and fmt is None: 

1084 return self.format_value(instance, config_id=None) 

1085 elif stringifier is None: 

1086 assert fmt is not None 

1087 try: 

1088 return instance.type.expand_value(self, instance.get_value(config_id=None), format_spec=fmt) 

1089 except Exception as e: 

1090 raise ParseException(e) 

1091 

1092 val: object 

1093 if stringifier == '': 

1094 val = self.format_value(instance, config_id=None) 

1095 else: 

1096 val = instance.get_value(config_id=None) 

1097 if stringifier == 'r': 

1098 val = repr(val) 

1099 elif stringifier == 's': 

1100 val = str(val) 

1101 elif stringifier == 'a': 

1102 val = ascii(val) 

1103 else: 

1104 raise ParseException('invalid conversion %r' % stringifier) 

1105 

1106 if fmt is None: 

1107 assert isinstance(val, str) 

1108 return val 

1109 

1110 try: 

1111 return format(val, fmt) 

1112 except ValueError as e: 

1113 raise ParseException(e) 

1114 

1115 def expand_env_match(self, m: 're.Match[str]') -> str: 

1116 ''' 

1117 :param m: A match of :attr:`~confattr.configfile.ConfigFile.reo_env`, group 1 is the name of the environment variable possibly including one of the following expansion features 

1118 :return: The expanded form of the environment variable 

1119 

1120 Supported are the following `parameter expansion features as defined by POSIX <https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_02>`__, except that word is not expanded: 

1121 

1122 - ``${parameter:-word}``/``${parameter-word}``: Use Default Values. If parameter is unset (or empty), word shall be substituted; otherwise, the value of parameter shall be substituted. 

1123 - ``${parameter:=word}``/``${parameter=word}``: Assign Default Values. If parameter is unset (or empty), word shall be assigned to parameter. In all cases, the final value of parameter shall be substituted. 

1124 - ``${parameter:?[word]}``/``${parameter?[word]}``: Indicate Error If Unset (or Empty). If parameter is unset (or empty), a :class:`~confattr.configfile.ParseException` shall be raised with word as message or a default error message if word is omitted. Otherwise, the value of parameter shall be substituted. 

1125 - ``${parameter:+word}``/``${parameter+word}``: Use Alternative Value. If parameter is unset (or empty), empty shall be substituted; otherwise, the expansion of word shall be substituted. 

1126 

1127 In the patterns above, if you use a ``:`` it is checked whether parameter is unset or empty. 

1128 If ``:`` is not used the check is only true if parameter is unset, empty is treated as a valid value. 

1129 ''' 

1130 env = m.group(1) 

1131 for op in '-=?+': 

1132 if ':' + op in env: 

1133 env, arg = env.split(':' + op, 1) 

1134 isset = bool(os.environ.get(env)) 

1135 elif op in env: 

1136 env, arg = env.split(op, 1) 

1137 isset = env in os.environ 

1138 else: 

1139 continue 

1140 

1141 val = os.environ.get(env, '') 

1142 if op == '-': 

1143 if isset: 

1144 return val 

1145 else: 

1146 return arg 

1147 elif op == '=': 

1148 if isset: 

1149 return val 

1150 else: 

1151 os.environ[env] = arg 

1152 return arg 

1153 elif op == '?': 

1154 if isset: 

1155 return val 

1156 else: 

1157 if not arg: 

1158 state = 'empty' if env in os.environ else 'unset' 

1159 arg = f'environment variable {env} is {state}' 

1160 raise ParseException(arg) 

1161 elif op == '+': 

1162 if isset: 

1163 return arg 

1164 else: 

1165 return '' 

1166 else: 

1167 assert False 

1168 

1169 return os.environ.get(env, '') 

1170 

1171 

1172 # ------- help ------- 

1173 

1174 def write_help(self, writer: FormattedWriter) -> None: 

1175 import platform 

1176 formatter = self.create_formatter() 

1177 writer.write_lines('The first existing file of the following paths is loaded:') 

1178 for path in self.iter_config_paths(): 

1179 writer.write_line('- %s' % path) 

1180 

1181 writer.write_line('') 

1182 writer.write_line('This can be influenced with the following environment variables:') 

1183 if platform.system() == 'Linux': # pragma: no branch 

1184 writer.write_line('- XDG_CONFIG_HOME') 

1185 writer.write_line('- XDG_CONFIG_DIRS') 

1186 for env in self.env_variables: 

1187 writer.write_line(f'- {env}') 

1188 

1189 writer.write_line('') 

1190 writer.write_lines(formatter.format_text(f'''\ 

1191You can also use environment variables to change the values of the settings listed under `set` command. 

1192The corresponding environment variable name is the name of the setting in all upper case letters 

1193with dots, hypens and spaces replaced by underscores and prefixed with "{self.envprefix}".''')) 

1194 

1195 writer.write_lines(formatter.format_text('Lines in the config file which start with a %s are ignored.' % ' or '.join('`%s`' % c for c in self.COMMENT_PREFIXES))) 

1196 

1197 writer.write_lines('The config file may contain the following commands:') 

1198 for cmd in self.commands: 

1199 names = '|'.join(cmd.get_names()) 

1200 writer.write_heading(SectionLevel.SECTION, names) 

1201 writer.write_lines(cmd.get_help()) 

1202 

1203 def create_formatter(self) -> HelpFormatterWrapper: 

1204 return HelpFormatterWrapper(self.formatter_class) 

1205 

1206 def get_help(self) -> str: 

1207 ''' 

1208 A convenience wrapper around :meth:`~confattr.configfile.ConfigFile.write_help` 

1209 to return the help as a str instead of writing it to a file. 

1210 

1211 This uses :class:`~confattr.configfile.HelpWriter`. 

1212 ''' 

1213 doc = io.StringIO() 

1214 self.write_help(HelpWriter(doc)) 

1215 # The generated help ends with a \n which is implicitly added by print. 

1216 # If I was writing to stdout or a file that would be desired. 

1217 # But if I return it as a string and then print it, the print adds another \n which would be too much. 

1218 # Therefore I am stripping the trailing \n. 

1219 return doc.getvalue().rstrip('\n') 

1220 

1221 

1222 # ------- auto complete ------- 

1223 

1224 def get_completions(self, line: str, cursor_pos: int) -> 'tuple[str, list[str], str]': 

1225 ''' 

1226 Provide an auto completion for commands that can be executed with :meth:`~confattr.configfile.ConfigFile.parse_line`. 

1227 

1228 :param line: The entire line that is currently in the text input field 

1229 :param cursor_pos: The position of the cursor 

1230 :return: start of line, completions, end of line. 

1231 *completions* is a list of possible completions for the word where the cursor is located. 

1232 If *completions* is an empty list there are no completions available and the user input should not be changed. 

1233 If *completions* is not empty it should be displayed by a user interface in a drop down menu. 

1234 The *start of line* is everything on the line before the completions. 

1235 The *end of line* is everything on the line after the completions. 

1236 In the likely case that the cursor is at the end of the line the *end of line* is an empty str. 

1237 *start of line* and *end of line* should be the beginning and end of :paramref:`~confattr.configfile.ConfigFile.get_completions.line` but they may contain minor changes in order to keep quoting feasible. 

1238 ''' 

1239 original_ln = line 

1240 stripped_line = line.lstrip() 

1241 indentation = line[:len(line) - len(stripped_line)] 

1242 cursor_pos -= len(indentation) 

1243 line = stripped_line 

1244 if self.enable_config_ids and line.startswith(self.ENTER_GROUP_PREFIX): 

1245 out = self.get_completions_enter_group(line, cursor_pos) 

1246 else: 

1247 out = self.get_completions_command(line, cursor_pos) 

1248 

1249 out = (indentation + out[0], out[1], out[2]) 

1250 return out 

1251 

1252 def get_completions_enter_group(self, line: str, cursor_pos: int) -> 'tuple[str, list[str], str]': 

1253 ''' 

1254 For a description of parameters and return type see :meth:`~confattr.configfile.ConfigFile.get_completions`. 

1255 

1256 :meth:`~confattr.configfile.ConfigFile.get_completions` has stripped any indentation from :paramref:`~confattr.configfile.ConfigFile.get_completions_enter_group.line` 

1257 and will prepend it to the first item of the return value. 

1258 ''' 

1259 start = line 

1260 groups = [self.ENTER_GROUP_PREFIX + str(cid) + self.ENTER_GROUP_SUFFIX for cid in MultiConfig.config_ids] 

1261 groups = [cid for cid in groups if cid.startswith(start)] 

1262 return '', groups, '' 

1263 

1264 def get_completions_command(self, line: str, cursor_pos: int) -> 'tuple[str, list[str], str]': 

1265 ''' 

1266 For a description of parameters and return type see :meth:`~confattr.configfile.ConfigFile.get_completions`. 

1267 

1268 :meth:`~confattr.configfile.ConfigFile.get_completions` has stripped any indentation from :paramref:`~confattr.configfile.ConfigFile.get_completions_command.line` 

1269 and will prepend it to the first item of the return value. 

1270 ''' 

1271 if not line: 

1272 return self.get_completions_command_name(line, cursor_pos, start_of_line='', end_of_line='') 

1273 

1274 ln_split = self.split_line_ignore_errors(line) 

1275 assert ln_split 

1276 a = self.find_arg(line, ln_split, cursor_pos) 

1277 

1278 if a.in_between: 

1279 start_of_line = line[:cursor_pos] 

1280 end_of_line = line[cursor_pos:] 

1281 else: 

1282 start_of_line = line[:a.i0] 

1283 end_of_line = line[a.i1:] 

1284 

1285 if a.argument_pos == 0: 

1286 return self.get_completions_command_name(line, cursor_pos, start_of_line=start_of_line, end_of_line=end_of_line) 

1287 else: 

1288 cmd = self.get_command(ln_split) 

1289 return cmd.get_completions(ln_split, a.argument_pos, cursor_pos-a.i0, in_between=a.in_between, start_of_line=start_of_line, end_of_line=end_of_line) 

1290 

1291 def find_arg(self, line: str, ln_split: 'list[str]', cursor_pos: int) -> ArgPos: 

1292 ''' 

1293 This is an internal method used by :meth:`~confattr.configfile.ConfigFile.get_completions_command` 

1294 ''' 

1295 CHARS_REMOVED_BY_SHLEX = ('"', "'", '\\') 

1296 assert cursor_pos <= len(line) # yes, cursor_pos can be == len(str) 

1297 out = ArgPos() 

1298 out.in_between = True 

1299 

1300 # init all out attributes just to be save, these should not never be used because line is not empty and not white space only 

1301 out.argument_pos = 0 

1302 out.i0 = 0 

1303 out.i1 = 0 

1304 

1305 n_ln = len(line) 

1306 i_ln = 0 

1307 n_arg = len(ln_split) 

1308 out.argument_pos = 0 

1309 i_in_arg = 0 

1310 assert out.argument_pos < n_ln 

1311 while True: 

1312 if out.in_between: 

1313 assert i_in_arg == 0 

1314 if i_ln >= n_ln: 

1315 assert out.argument_pos >= n_arg - 1 

1316 out.i0 = i_ln 

1317 return out 

1318 elif line[i_ln].isspace(): 

1319 i_ln += 1 

1320 else: 

1321 out.i0 = i_ln 

1322 if i_ln >= cursor_pos: 

1323 return out 

1324 if out.argument_pos >= n_arg: 

1325 assert line[i_ln] == '#' 

1326 out.i0 = len(line) 

1327 return out 

1328 out.in_between = False 

1329 else: 

1330 if i_ln >= n_ln: 

1331 assert out.argument_pos >= n_arg - 1 

1332 out.i1 = i_ln 

1333 return out 

1334 elif i_in_arg >= len(ln_split[out.argument_pos]): 

1335 if line[i_ln].isspace(): 

1336 out.i1 = i_ln 

1337 if i_ln >= cursor_pos: 

1338 return out 

1339 out.in_between = True 

1340 i_ln += 1 

1341 out.argument_pos += 1 

1342 i_in_arg = 0 

1343 elif line[i_ln] in CHARS_REMOVED_BY_SHLEX: 

1344 i_ln += 1 

1345 else: 

1346 # unlike bash shlex treats a comment character inside of an argument as a comment character 

1347 assert line[i_ln] == '#' 

1348 assert out.argument_pos == n_arg - 1 

1349 out.i1 = i_ln 

1350 return out 

1351 elif line[i_ln] == ln_split[out.argument_pos][i_in_arg]: 

1352 i_ln += 1 

1353 i_in_arg += 1 

1354 if out.argument_pos == 0 and i_ln == 1 and self.split_one_symbol_command(line)[0]: 

1355 out.in_between = True 

1356 out.argument_pos += 1 

1357 out.i0 = i_ln 

1358 i_in_arg = 0 

1359 else: 

1360 assert line[i_ln] in CHARS_REMOVED_BY_SHLEX 

1361 i_ln += 1 

1362 

1363 

1364 def get_completions_command_name(self, line: str, cursor_pos: int, *, start_of_line: str, end_of_line: str) -> 'tuple[str, list[str], str]': 

1365 start = line[:cursor_pos] 

1366 completions = [cmd for cmd in self.command_dict.keys() if cmd.startswith(start) and len(cmd) > 1] 

1367 return start_of_line, completions, end_of_line 

1368 

1369 

1370 def get_completions_for_file_name(self, start: str, *, relative_to: str, include: 'Callable[[str, str], bool]|None' = None, exclude: 'str|None' = None, match: 'Callable[[str, str, str], bool]' = lambda path, name, start: name.startswith(start), start_of_line: str, end_of_line: str) -> 'tuple[str, list[str], str]': 

1371 r''' 

1372 :param start: The start of the path to be completed 

1373 :param relative_to: If :paramref:`~confattr.configfile.ConfigFile.get_completions_for_file_name.start` is a relative path it's relative to this directory 

1374 :param exclude: A regular expression. The default value :obj:`None` is interpreted differently depending on the :func:`platform.platform`. For ``Windows`` it's ``$none`` so that nothing is excluded. For others it's ``^\.`` so that hidden files and directories are excluded. 

1375 :param include: A function which takes the path and file name as arguments and returns whether this file/directory is a valid completion. 

1376 :param match: A callable to decide if a completion fits for the given start. It takes three arguments: the parent directory, the file/directory name and the start. If it returns true the file/direcotry is added to the list of possible completions. The default is ``lambda path, name, start: name.startswith(start)``. 

1377 :return: All files and directories that start with :paramref:`~confattr.configfile.ConfigFile.get_completions_for_file_name.start` and do not match :paramref:`~confattr.configfile.ConfigFile.get_completions_for_file_name.exclude`. Directories are appended with :const:`os.path.sep`. :const:`os.path.sep` is appended after quoting so that it can be easily stripped if undesired (e.g. if the user interface cycles through all possible completions instead of completing the longest common prefix). 

1378 ''' 

1379 if exclude is None: 

1380 if platform.platform() == 'Windows' or os.path.split(start)[1].startswith('.'): 

1381 exclude = '$none' 

1382 else: 

1383 exclude = r'^\.' 

1384 reo = re.compile(exclude) 

1385 

1386 # I cannot use os.path.split because that would ignore the important difference between having a trailing separator or not 

1387 if os.path.sep in start: 

1388 directory, start = start.rsplit(os.path.sep, 1) 

1389 directory += os.path.sep 

1390 quoted_directory = self.quote_path(directory) 

1391 

1392 start_of_line += quoted_directory 

1393 directory = os.path.expanduser(directory) 

1394 if not os.path.isabs(directory): 

1395 directory = os.path.join(relative_to, directory) 

1396 directory = os.path.normpath(directory) 

1397 else: 

1398 directory = relative_to 

1399 

1400 try: 

1401 names = os.listdir(directory) 

1402 except (FileNotFoundError, NotADirectoryError): 

1403 return start_of_line, [], end_of_line 

1404 

1405 out: 'list[str]' = [] 

1406 for name in names: 

1407 if reo.match(name): 

1408 continue 

1409 if include and not include(directory, name): 

1410 continue 

1411 if not match(directory, name, start): 

1412 continue 

1413 

1414 quoted_name = self.quote(name) 

1415 if os.path.isdir(os.path.join(directory, name)): 

1416 quoted_name += os.path.sep 

1417 

1418 out.append(quoted_name) 

1419 

1420 return start_of_line, out, end_of_line 

1421 

1422 def quote_path(self, path: str) -> str: 

1423 path_split = path.split(os.path.sep) 

1424 i0 = 1 if path_split[0] == '~' else 0 

1425 for i in range(i0, len(path_split)): 

1426 if path_split[i]: 

1427 path_split[i] = self.quote(path_split[i]) 

1428 return os.path.sep.join(path_split) 

1429 

1430 

1431 def get_completions_for_expand(self, start: str, *, start_of_line: str, end_of_line: str) -> 'tuple[bool, str, list[str], str]': 

1432 applicable, start_of_line, completions, end_of_line = self.get_completions_for_expand_env(start, start_of_line=start_of_line, end_of_line=end_of_line) 

1433 if applicable: 

1434 return applicable, start_of_line, completions, end_of_line 

1435 

1436 return self.get_completions_for_expand_config(start, start_of_line=start_of_line, end_of_line=end_of_line) 

1437 

1438 def get_completions_for_expand_config(self, start: str, *, start_of_line: str, end_of_line: str) -> 'tuple[bool, str, list[str], str]': 

1439 if start.count('%') % 2 == 0: 

1440 return False, start_of_line, [], end_of_line 

1441 

1442 i = start.rindex('%') + 1 

1443 start_of_line = start_of_line + start[:i] 

1444 start = start[i:] 

1445 completions = [key for key in sorted(self.config_instances.keys()) if key.startswith(start)] 

1446 return True, start_of_line, completions, end_of_line 

1447 

1448 def get_completions_for_expand_env(self, start: str, *, start_of_line: str, end_of_line: str) -> 'tuple[bool, str, list[str], str]': 

1449 i = start.rfind('${') 

1450 if i < 0: 

1451 return False, start_of_line, [], end_of_line 

1452 i += 2 

1453 

1454 if '}' in start[i:]: 

1455 return False, start_of_line, [], end_of_line 

1456 

1457 start_of_line = start_of_line + start[:i] 

1458 start = start[i:] 

1459 completions = [key for key in sorted(os.environ.keys()) if key.startswith(start)] 

1460 return True, start_of_line, completions, end_of_line 

1461 

1462 

1463 # ------- error handling ------- 

1464 

1465 def parse_error(self, msg: str) -> None: 

1466 ''' 

1467 Is called if something went wrong while trying to load a config file. 

1468 

1469 This method is called when a :class:`~confattr.configfile.ParseException` or :class:`~confattr.configfile.MultipleParseExceptions` is caught. 

1470 This method compiles the given information into an error message and calls :meth:`self.ui_notifier.show_error() <confattr.configfile.UiNotifier.show_error>`. 

1471 

1472 :param msg: The error message 

1473 ''' 

1474 self.ui_notifier.show_error(msg) 

1475 

1476 

1477# ---------- base classes for commands which can be used in config files ---------- 

1478 

1479class ConfigFileCommand(abc.ABC): 

1480 

1481 ''' 

1482 An abstract base class for commands which can be used in a config file. 

1483 

1484 Subclasses must implement the :meth:`~confattr.configfile.ConfigFileCommand.run` method which is called when :class:`~confattr.configfile.ConfigFile` is loading a file. 

1485 Subclasses should contain a doc string so that :meth:`~confattr.configfile.ConfigFileCommand.get_help` can provide a description to the user. 

1486 Subclasses may set the :attr:`~confattr.configfile.ConfigFileCommand.name` and :attr:`~confattr.configfile.ConfigFileCommand.aliases` attributes to change the output of :meth:`~confattr.configfile.ConfigFileCommand.get_name` and :meth:`~confattr.configfile.ConfigFileCommand.get_names`. 

1487 

1488 All subclasses are remembered and can be retrieved with :meth:`~confattr.configfile.ConfigFileCommand.get_command_types`. 

1489 They are instantiated in the constructor of :class:`~confattr.configfile.ConfigFile`. 

1490 ''' 

1491 

1492 #: The name which is used in the config file to call this command. Use an empty string to define a default command which is used if an undefined command is encountered. If this is not set :meth:`~confattr.configfile.ConfigFileCommand.get_name` returns the name of this class in lower case letters and underscores replaced by hyphens. 

1493 name: str 

1494 

1495 #: Alternative names which can be used in the config file. 

1496 aliases: 'tuple[str, ...]|list[str]' 

1497 

1498 #: A description which may be used by an in-app help. If this is not set :meth:`~confattr.configfile.ConfigFileCommand.get_help` uses the doc string instead. 

1499 help: str 

1500 

1501 #: If a config file contains only a single section it makes no sense to write a heading for it. This attribute is set by :meth:`ConfigFile.save_to_writer() <confattr.configfile.ConfigFile.save_to_writer>` if there are several commands which implement the :meth:`~confattr.configfile.ConfigFileCommand.save` method. If you implement :meth:`~confattr.configfile.ConfigFileCommand.save` and this attribute is set then :meth:`~confattr.configfile.ConfigFileCommand.save` should write a section header. If :meth:`~confattr.configfile.ConfigFileCommand.save` writes several sections it should always write the headings regardless of this attribute. 

1502 should_write_heading: bool = False 

1503 

1504 #: The :class:`~confattr.configfile.ConfigFile` that has been passed to the constructor. It determines for example the :paramref:`~confattr.configfile.ConfigFile.notification_level` and the available :paramref:`~confattr.configfile.ConfigFile.commands`. 

1505 config_file: ConfigFile 

1506 

1507 #: The :class:`~confattr.configfile.UiNotifier` of :attr:`~confattr.configfile.ConfigFileCommand.config_file` 

1508 ui_notifier: UiNotifier 

1509 

1510 

1511 _subclasses: 'list[type[ConfigFileCommand]]' = [] 

1512 _used_names: 'set[str]' = set() 

1513 

1514 @classmethod 

1515 def get_command_types(cls) -> 'tuple[type[ConfigFileCommand], ...]': 

1516 ''' 

1517 :return: All subclasses of :class:`~confattr.configfile.ConfigFileCommand` which have not been deleted with :meth:`~confattr.configfile.ConfigFileCommand.delete_command_type` 

1518 ''' 

1519 return tuple(cls._subclasses) 

1520 

1521 @classmethod 

1522 def delete_command_type(cls, cmd_type: 'type[ConfigFileCommand]') -> None: 

1523 ''' 

1524 Delete :paramref:`~confattr.configfile.ConfigFileCommand.delete_command_type.cmd_type` so that it is not returned anymore by :meth:`~confattr.configfile.ConfigFileCommand.get_command_types` and that it's name can be used by another command. 

1525 Do nothing if :paramref:`~confattr.configfile.ConfigFileCommand.delete_command_type.cmd_type` has already been deleted. 

1526 ''' 

1527 if cmd_type in cls._subclasses: 

1528 cls._subclasses.remove(cmd_type) 

1529 for name in cmd_type.get_names(): 

1530 cls._used_names.remove(name) 

1531 

1532 @classmethod 

1533 def __init_subclass__(cls, replace: bool = False, abstract: bool = False) -> None: 

1534 ''' 

1535 :param replace: Set :attr:`~confattr.configfile.ConfigFileCommand.name` and :attr:`~confattr.configfile.ConfigFileCommand.aliases` to the values of the parent class if they are not set explicitly, delete the parent class with :meth:`~confattr.configfile.ConfigFileCommand.delete_command_type` and replace any commands with the same name 

1536 :param abstract: This class is a base class for the implementation of other commands and shall *not* be returned by :meth:`~confattr.configfile.ConfigFileCommand.get_command_types` 

1537 :raises ValueError: if the name or one of it's aliases is already in use and :paramref:`~confattr.configfile.ConfigFileCommand.__init_subclass__.replace` is not true 

1538 ''' 

1539 if replace: 

1540 parent_commands = [parent for parent in cls.__bases__ if issubclass(parent, ConfigFileCommand)] 

1541 

1542 # set names of this class to that of the parent class(es) 

1543 parent = parent_commands[0] 

1544 if 'name' not in cls.__dict__: 

1545 cls.name = parent.get_name() 

1546 if 'aliases' not in cls.__dict__: 

1547 cls.aliases = list(parent.get_names())[1:] 

1548 for parent in parent_commands[1:]: 

1549 cls.aliases.extend(parent.get_names()) 

1550 

1551 # remove parent class from the list of commands to be loaded or saved 

1552 for parent in parent_commands: 

1553 cls.delete_command_type(parent) 

1554 

1555 if not abstract: 

1556 cls._subclasses.append(cls) 

1557 for name in cls.get_names(): 

1558 if name in cls._used_names and not replace: 

1559 raise ValueError('duplicate command name %r' % name) 

1560 cls._used_names.add(name) 

1561 

1562 @classmethod 

1563 def get_name(cls) -> str: 

1564 ''' 

1565 :return: The name which is used in config file to call this command. 

1566  

1567 If :attr:`~confattr.configfile.ConfigFileCommand.name` is set it is returned as it is. 

1568 Otherwise a name is generated based on the class name. 

1569 ''' 

1570 if 'name' in cls.__dict__: 

1571 return cls.name 

1572 return cls.__name__.lower().replace("_", "-") 

1573 

1574 @classmethod 

1575 def get_names(cls) -> 'Iterator[str]': 

1576 ''' 

1577 :return: Several alternative names which can be used in a config file to call this command. 

1578  

1579 The first one is always the return value of :meth:`~confattr.configfile.ConfigFileCommand.get_name`. 

1580 If :attr:`~confattr.configfile.ConfigFileCommand.aliases` is set it's items are yielded afterwards. 

1581 

1582 If one of the returned items is the empty string this class is the default command 

1583 and :meth:`~confattr.configfile.ConfigFileCommand.run` will be called if an undefined command is encountered. 

1584 ''' 

1585 yield cls.get_name() 

1586 if 'aliases' in cls.__dict__: 

1587 for name in cls.aliases: 

1588 yield name 

1589 

1590 def __init__(self, config_file: ConfigFile) -> None: 

1591 self.config_file = config_file 

1592 self.ui_notifier = config_file.ui_notifier 

1593 

1594 @abc.abstractmethod 

1595 def run(self, cmd: 'Sequence[str]') -> None: 

1596 ''' 

1597 Process one line which has been read from a config file 

1598 

1599 :raises ParseException: if there is an error in the line (e.g. invalid syntax) 

1600 :raises MultipleParseExceptions: if there are several errors in the same line 

1601 ''' 

1602 raise NotImplementedError() 

1603 

1604 

1605 def create_formatter(self) -> HelpFormatterWrapper: 

1606 return self.config_file.create_formatter() 

1607 

1608 def get_help_attr_or_doc_str(self) -> str: 

1609 ''' 

1610 :return: The :attr:`~confattr.configfile.ConfigFileCommand.help` attribute or the doc string if :attr:`~confattr.configfile.ConfigFileCommand.help` has not been set, cleaned up with :func:`inspect.cleandoc`. 

1611 ''' 

1612 if hasattr(self, 'help'): 

1613 doc = self.help 

1614 elif self.__doc__: 

1615 doc = self.__doc__ 

1616 else: 

1617 doc = '' 

1618 

1619 return inspect.cleandoc(doc) 

1620 

1621 def add_help_to(self, formatter: HelpFormatterWrapper) -> None: 

1622 ''' 

1623 Add the return value of :meth:`~confattr.configfile.ConfigFileCommand.get_help_attr_or_doc_str` to :paramref:`~confattr.configfile.ConfigFileCommand.add_help_to.formatter`. 

1624 ''' 

1625 formatter.add_text(self.get_help_attr_or_doc_str()) 

1626 

1627 def get_help(self) -> str: 

1628 ''' 

1629 :return: A help text which can be presented to the user. 

1630 

1631 This is generated by creating a formatter with :meth:`~confattr.configfile.ConfigFileCommand.create_formatter`, 

1632 adding the help to it with :meth:`~confattr.configfile.ConfigFileCommand.add_help_to` and 

1633 stripping trailing new line characters from the result of :meth:`HelpFormatterWrapper.format_help() <confattr.utils.HelpFormatterWrapper.format_help>`. 

1634 

1635 Most likely you don't want to override this method but :meth:`~confattr.configfile.ConfigFileCommand.add_help_to` instead. 

1636 ''' 

1637 formatter = self.create_formatter() 

1638 self.add_help_to(formatter) 

1639 return formatter.format_help().rstrip('\n') 

1640 

1641 def save(self, 

1642 writer: FormattedWriter, 

1643 **kw: 'Unpack[SaveKwargs]', 

1644 ) -> None: 

1645 ''' 

1646 Implement this method if you want calls to this command to be written by :meth:`ConfigFile.save() <confattr.configfile.ConfigFile.save>`. 

1647 

1648 If you implement this method write a section heading with :meth:`writer.write_heading('Heading') <confattr.configfile.FormattedWriter.write_heading>` if :attr:`~confattr.configfile.ConfigFileCommand.should_write_heading` is true. 

1649 If this command writes several sections then write a heading for every section regardless of :attr:`~confattr.configfile.ConfigFileCommand.should_write_heading`. 

1650 

1651 Write as many calls to this command as necessary to the config file in order to create the current state with :meth:`writer.write_command('...') <confattr.configfile.FormattedWriter.write_command>`. 

1652 Write comments or help with :meth:`writer.write_lines('...') <confattr.configfile.FormattedWriter.write_lines>`. 

1653 

1654 There is the :attr:`~confattr.configfile.ConfigFileCommand.config_file` attribute (which was passed to the constructor) which you can use to: 

1655 

1656 - quote arguments with :meth:`ConfigFile.quote() <confattr.configfile.ConfigFile.quote>` 

1657 - call :meth:`ConfigFile.write_config_id() <confattr.configfile.ConfigFile.write_config_id>` 

1658 

1659 You probably don't need the comment character :attr:`ConfigFile.COMMENT <confattr.configfile.ConfigFile.COMMENT>` because :paramref:`~confattr.configfile.ConfigFileCommand.save.writer` automatically comments out everything except for :meth:`FormattedWriter.write_command() <confattr.configfile.FormattedWriter.write_command>`. 

1660 

1661 The default implementation does nothing. 

1662 ''' 

1663 pass 

1664 

1665 save.implemented = False # type: ignore [attr-defined] 

1666 

1667 

1668 # ------- auto complete ------- 

1669 

1670 def get_completions(self, cmd: 'Sequence[str]', argument_pos: int, cursor_pos: int, *, in_between: bool, start_of_line: str, end_of_line: str) -> 'tuple[str, list[str], str]': 

1671 ''' 

1672 :param cmd: The line split into arguments (including the name of this command as cmd[0]) 

1673 :param argument_pos: The index of the argument which shall be completed. Please note that this can be one bigger than :paramref:`~confattr.configfile.ConfigFileCommand.get_completions.cmd` is long if the line ends on a space and the cursor is behind that space. In that case :paramref:`~confattr.configfile.ConfigFileCommand.get_completions.in_between` is true. 

1674 :param cursor_pos: The index inside of the argument where the cursor is located. This is undefined and should be ignored if :paramref:`~confattr.configfile.ConfigFileCommand.get_completions.in_between` is true. The input from the start of the argument to the cursor should be used to filter the completions. The input after the cursor can be ignored. 

1675 :param in_between: If true: The cursor is between two arguments, before the first argument or after the last argument. :paramref:`~confattr.configfile.ConfigFileCommand.get_completions.argument_pos` refers to the next argument, :paramref:`argument_pos-1 <confattr.configfile.ConfigFileCommand.get_completions.argument_pos>` to the previous argument. :paramref:`~confattr.configfile.ConfigFileCommand.get_completions.cursor_pos` is undefined. 

1676 :param start_of_line: The first return value. If ``cmd[argument_pos]`` has a pattern like ``key=value`` you can append ``key=`` to this value and return only completions of ``value`` as second return value. 

1677 :param end_of_line: The third return value. 

1678 :return: start of line, completions, end of line. 

1679 *completions* is a list of possible completions for the word where the cursor is located. 

1680 If *completions* is an empty list there are no completions available and the user input should not be changed. 

1681 This should be displayed by a user interface in a drop down menu. 

1682 The *start of line* is everything on the line before the completions. 

1683 The *end of line* is everything on the line after the completions. 

1684 In the likely case that the cursor is at the end of the line the *end of line* is an empty str. 

1685 ''' 

1686 completions: 'list[str]' = [] 

1687 return start_of_line, completions, end_of_line 

1688 

1689 

1690class ArgumentParser(argparse.ArgumentParser): 

1691 

1692 def error(self, message: str) -> 'typing.NoReturn': 

1693 ''' 

1694 Raise a :class:`~confattr.configfile.ParseException`. 

1695 ''' 

1696 raise ParseException(message) 

1697 

1698class ConfigFileArgparseCommand(ConfigFileCommand, abstract=True): 

1699 

1700 ''' 

1701 An abstract subclass of :class:`~confattr.configfile.ConfigFileCommand` which uses :mod:`argparse` to make parsing and providing help easier. 

1702 

1703 You must implement the class method :meth:`~confattr.configfile.ConfigFileArgparseCommand.init_parser` to add the arguments to :attr:`~confattr.configfile.ConfigFileArgparseCommand.parser`. 

1704 Instead of :meth:`~confattr.configfile.ConfigFileArgparseCommand.run` you must implement :meth:`~confattr.configfile.ConfigFileArgparseCommand.run_parsed`. 

1705 You don't need to add a usage or the possible arguments to the doc string as :mod:`argparse` will do that for you. 

1706 You should, however, still give a description what this command does in the doc string. 

1707 

1708 You may specify :attr:`ConfigFileCommand.name <confattr.configfile.ConfigFileCommand.name>`, :attr:`ConfigFileCommand.aliases <confattr.configfile.ConfigFileCommand.aliases>` and :meth:`ConfigFileCommand.save() <confattr.configfile.ConfigFileCommand.save>` like for :class:`~confattr.configfile.ConfigFileCommand`. 

1709 ''' 

1710 

1711 #: The argument parser which is passed to :meth:`~confattr.configfile.ConfigFileArgparseCommand.init_parser` for adding arguments and which is used in :meth:`~confattr.configfile.ConfigFileArgparseCommand.run` 

1712 parser: ArgumentParser 

1713 

1714 def __init__(self, config_file: ConfigFile) -> None: 

1715 super().__init__(config_file) 

1716 self._names = set(self.get_names()) 

1717 self.parser = ArgumentParser(prog=self.get_name(), description=self.get_help_attr_or_doc_str(), add_help=False, formatter_class=self.config_file.formatter_class) 

1718 self.init_parser(self.parser) 

1719 

1720 @abc.abstractmethod 

1721 def init_parser(self, parser: ArgumentParser) -> None: 

1722 ''' 

1723 :param parser: The parser to add arguments to. This is the same object like :attr:`~confattr.configfile.ConfigFileArgparseCommand.parser`. 

1724 

1725 This is an abstract method which must be implemented by subclasses. 

1726 Use :meth:`ArgumentParser.add_argument() <confattr.configfile.ArgumentParser.add_argument>` to add arguments to :paramref:`~confattr.configfile.ConfigFileArgparseCommand.init_parser.parser`. 

1727 ''' 

1728 pass 

1729 

1730 def get_help(self) -> str: 

1731 ''' 

1732 Creates a help text which can be presented to the user by calling :meth:`~confattr.configfile.ArgumentParser.format_help` on :attr:`~confattr.configfile.ConfigFileArgparseCommand.parser`. 

1733 The return value of :meth:`~confattr.configfile.ConfigFileArgparseCommand.get_help_attr_or_doc_str` has been passed as :paramref:`~confattr.configfile.ArgumentParser.description` to the constructor of :class:`~confattr.configfile.ArgumentParser`, therefore :attr:`~confattr.configfile.ConfigFileArgparseCommand.help`/the doc string are included as well. 

1734 ''' 

1735 return self.parser.format_help().rstrip('\n') 

1736 

1737 def run(self, cmd: 'Sequence[str]') -> None: 

1738 # if the line was empty this method should not be called but an empty line should be ignored either way 

1739 if not cmd: 

1740 return # pragma: no cover 

1741 # cmd[0] does not need to be in self._names if this is the default command, i.e. if '' in self._names 

1742 if cmd[0] in self._names: 

1743 cmd = cmd[1:] 

1744 args = self.parser.parse_args(cmd) 

1745 self.run_parsed(args) 

1746 

1747 @abc.abstractmethod 

1748 def run_parsed(self, args: argparse.Namespace) -> None: 

1749 ''' 

1750 This is an abstract method which must be implemented by subclasses. 

1751 ''' 

1752 pass 

1753 

1754 # ------- auto complete ------- 

1755 

1756 def get_completions(self, cmd: 'Sequence[str]', argument_pos: int, cursor_pos: int, *, in_between: bool, start_of_line: str, end_of_line: str) -> 'tuple[str, list[str], str]': 

1757 if in_between: 

1758 start = '' 

1759 else: 

1760 start = cmd[argument_pos][:cursor_pos] 

1761 

1762 if self.after_positional_argument_marker(cmd, argument_pos): 

1763 pos = self.get_position(cmd, argument_pos) 

1764 return self.get_completions_for_positional_argument(pos, start, start_of_line=start_of_line, end_of_line=end_of_line) 

1765 

1766 if argument_pos > 0: # pragma: no branch # if argument_pos was 0 this method would not be called, command names would be completed instead 

1767 prevarg = self.get_option_name_if_it_takes_an_argument(cmd, argument_pos-1) 

1768 if prevarg: 

1769 return self.get_completions_for_option_argument(prevarg, start, start_of_line=start_of_line, end_of_line=end_of_line) 

1770 

1771 if self.is_option_start(start): 

1772 if '=' in start: 

1773 i = start.index('=') 

1774 option_name = start[:i] 

1775 i += 1 

1776 start_of_line += start[:i] 

1777 start = start[i:] 

1778 return self.get_completions_for_option_argument(option_name, start, start_of_line=start_of_line, end_of_line=end_of_line) 

1779 return self.get_completions_for_option_name(start, start_of_line=start_of_line, end_of_line=end_of_line) 

1780 

1781 pos = self.get_position(cmd, argument_pos) 

1782 return self.get_completions_for_positional_argument(pos, start, start_of_line=start_of_line, end_of_line=end_of_line) 

1783 

1784 def get_position(self, cmd: 'Sequence[str]', argument_pos: int) -> int: 

1785 ''' 

1786 :return: the position of a positional argument, not counting options and their arguments 

1787 ''' 

1788 pos = 0 

1789 n = len(cmd) 

1790 options_allowed = True 

1791 # I am starting at 1 because cmd[0] is the name of the command, not an argument 

1792 for i in range(1, argument_pos): 

1793 if options_allowed and i < n: 

1794 if cmd[i] == '--': 

1795 options_allowed = False 

1796 continue 

1797 elif self.is_option_start(cmd[i]): 

1798 continue 

1799 # > 1 because cmd[0] is the name of the command 

1800 elif i > 1 and self.get_option_name_if_it_takes_an_argument(cmd, i-1): 

1801 continue 

1802 pos += 1 

1803 

1804 return pos 

1805 

1806 def is_option_start(self, start: str) -> bool: 

1807 return start.startswith('-') or start.startswith('+') 

1808 

1809 def after_positional_argument_marker(self, cmd: 'Sequence[str]', argument_pos: int) -> bool: 

1810 ''' 

1811 :return: true if this can only be a positional argument. False means it can be both, option or positional argument. 

1812 ''' 

1813 return '--' in cmd and cmd.index('--') < argument_pos 

1814 

1815 def get_option_name_if_it_takes_an_argument(self, cmd: 'Sequence[str]', argument_pos: int) -> 'str|None': 

1816 if argument_pos >= len(cmd): 

1817 return None # pragma: no cover # this does not happen because this method is always called for the previous argument 

1818 

1819 arg = cmd[argument_pos] 

1820 if '=' in arg: 

1821 # argument of option is already given within arg 

1822 return None 

1823 if not self.is_option_start(arg): 

1824 return None 

1825 if arg.startswith('--'): 

1826 action = self.get_action_for_option(arg) 

1827 if action is None: 

1828 return None 

1829 if action.nargs != 0: 

1830 return arg 

1831 return None 

1832 

1833 # arg is a combination of single character flags like in `tar -xzf file` 

1834 for c in arg[1:-1]: 

1835 action = self.get_action_for_option('-' + c) 

1836 if action is None: 

1837 continue 

1838 if action.nargs != 0: 

1839 # c takes an argument but that is already given within arg 

1840 return None 

1841 

1842 out = '-' + arg[-1] 

1843 action = self.get_action_for_option(out) 

1844 if action is None: 

1845 return None 

1846 if action.nargs != 0: 

1847 return out 

1848 return None 

1849 

1850 

1851 def get_completions_for_option_name(self, start: str, *, start_of_line: str, end_of_line: str) -> 'tuple[str, list[str], str]': 

1852 completions = [] 

1853 for a in self.parser._get_optional_actions(): 

1854 for opt in a.option_strings: 

1855 if len(opt) <= 2: 

1856 # this is trivial to type but not self explanatory 

1857 # => not helpful for auto completion 

1858 continue 

1859 if opt.startswith(start): 

1860 completions.append(opt) 

1861 return start_of_line, completions, end_of_line 

1862 

1863 def get_completions_for_option_argument(self, option_name: str, start: str, *, start_of_line: str, end_of_line: str) -> 'tuple[str, list[str], str]': 

1864 return self.get_completions_for_action(self.get_action_for_option(option_name), start, start_of_line=start_of_line, end_of_line=end_of_line) 

1865 

1866 def get_completions_for_positional_argument(self, position: int, start: str, *, start_of_line: str, end_of_line: str) -> 'tuple[str, list[str], str]': 

1867 return self.get_completions_for_action(self.get_action_for_positional_argument(position), start, start_of_line=start_of_line, end_of_line=end_of_line) 

1868 

1869 

1870 def get_action_for_option(self, option_name: str) -> 'argparse.Action|None': 

1871 for a in self.parser._get_optional_actions(): 

1872 if option_name in a.option_strings: 

1873 return a 

1874 return None 

1875 

1876 def get_action_for_positional_argument(self, argument_pos: int) -> 'argparse.Action|None': 

1877 actions = self.parser._get_positional_actions() 

1878 if argument_pos < len(actions): 

1879 return actions[argument_pos] 

1880 return None 

1881 

1882 def get_completions_for_action(self, action: 'argparse.Action|None', start: str, *, start_of_line: str, end_of_line: str) -> 'tuple[str, list[str], str]': 

1883 if action is None: 

1884 completions: 'list[str]' = [] 

1885 elif not action.choices: 

1886 completions = [] 

1887 else: 

1888 completions = [str(val) for val in action.choices] 

1889 completions = [val for val in completions if val.startswith(start)] 

1890 completions = [self.config_file.quote(val) for val in completions] 

1891 return start_of_line, completions, end_of_line 

1892 

1893 

1894# ---------- implementations of commands which can be used in config files ---------- 

1895 

1896class Set(ConfigFileCommand): 

1897 

1898 r''' 

1899 usage: set [--raw] key1=val1 [key2=val2 ...] \\ 

1900 set [--raw] key [=] val 

1901 

1902 Change the value of a setting. 

1903 

1904 In the first form set takes an arbitrary number of arguments, each argument sets one setting. 

1905 This has the advantage that several settings can be changed at once. 

1906 That is useful if you want to bind a set command to a key and process that command with ConfigFile.parse_line() if the key is pressed. 

1907 

1908 In the second form set takes two arguments, the key and the value. Optionally a single equals character may be added in between as third argument. 

1909 This has the advantage that key and value are separated by one or more spaces which can improve the readability of a config file. 

1910 

1911 You can use the value of another setting with %other.key% or an environment variable with ${ENV_VAR}. 

1912 If you want to insert a literal percent character use two of them: %%. 

1913 You can disable expansion of settings and environment variables with the --raw flag. 

1914 ''' 

1915 

1916 #: The separator which is used between a key and it's value 

1917 KEY_VAL_SEP = '=' 

1918 

1919 FLAGS_RAW = ('-r', '--raw') 

1920 

1921 raw = False 

1922 

1923 # ------- load ------- 

1924 

1925 def run(self, cmd: 'Sequence[str]') -> None: 

1926 ''' 

1927 Call :meth:`~confattr.configfile.Set.set_multiple` if the first argument contains :attr:`~confattr.configfile.Set.KEY_VAL_SEP` otherwise :meth:`~confattr.configfile.Set.set_with_spaces`. 

1928 

1929 :raises ParseException: if something is wrong (no arguments given, invalid syntax, invalid key, invalid value) 

1930 ''' 

1931 if self.is_vim_style(cmd): 

1932 self.set_multiple(cmd) 

1933 else: 

1934 self.set_with_spaces(cmd) 

1935 

1936 def is_vim_style(self, cmd: 'Sequence[str]') -> bool: 

1937 ''' 

1938 :paramref:`~confattr.configfile.Set.is_vim_style.cmd` has one of two possible styles: 

1939 - vim inspired: set takes an arbitrary number of arguments, each argument sets one setting. Is handled by :meth:`~confattr.configfile.Set.set_multiple`. 

1940 - ranger inspired: set takes two arguments, the key and the value. Optionally a single equals character may be added in between as third argument. Is handled by :meth:`~confattr.configfile.Set.set_with_spaces`. 

1941 

1942 :return: true if cmd has a vim inspired style, false if cmd has a ranger inspired style 

1943 ''' 

1944 try: 

1945 # cmd[0] is the name of the command, cmd[1] is the first argument 

1946 if cmd[1] in self.FLAGS_RAW: 

1947 i = 2 

1948 else: 

1949 i = 1 

1950 return self.KEY_VAL_SEP in cmd[i] 

1951 except IndexError: 

1952 raise ParseException('no settings given') 

1953 

1954 def set_with_spaces(self, cmd: 'Sequence[str]') -> None: 

1955 ''' 

1956 Process one line of the format ``set key [=] value`` 

1957 

1958 :raises ParseException: if something is wrong (invalid syntax, invalid key, invalid value) 

1959 ''' 

1960 if cmd[1] in self.FLAGS_RAW: 

1961 cmd = cmd[2:] 

1962 self.raw = True 

1963 else: 

1964 cmd = cmd[1:] 

1965 self.raw = False 

1966 

1967 n = len(cmd) 

1968 if n == 2: 

1969 key, value = cmd 

1970 self.parse_key_and_set_value(key, value) 

1971 elif n == 3: 

1972 key, sep, value = cmd 

1973 if sep != self.KEY_VAL_SEP: 

1974 raise ParseException(f'separator between key and value should be {self.KEY_VAL_SEP}, not {sep!r}') 

1975 self.parse_key_and_set_value(key, value) 

1976 elif n == 1: 

1977 raise ParseException(f'missing value or missing {self.KEY_VAL_SEP}') 

1978 else: 

1979 assert n >= 4 

1980 raise ParseException(f'too many arguments given or missing {self.KEY_VAL_SEP} in first argument') 

1981 

1982 def set_multiple(self, cmd: 'Sequence[str]') -> None: 

1983 ''' 

1984 Process one line of the format ``set key=value [key2=value2 ...]`` 

1985 

1986 :raises MultipleParseExceptions: if something is wrong (invalid syntax, invalid key, invalid value) 

1987 ''' 

1988 self.raw = False 

1989 exceptions = [] 

1990 for arg in cmd[1:]: 

1991 if arg in self.FLAGS_RAW: 

1992 self.raw = True 

1993 continue 

1994 try: 

1995 if not self.KEY_VAL_SEP in arg: 

1996 raise ParseException(f'missing {self.KEY_VAL_SEP} in {arg!r}') 

1997 key, value = arg.split(self.KEY_VAL_SEP, 1) 

1998 self.parse_key_and_set_value(key, value) 

1999 except ParseException as e: 

2000 exceptions.append(e) 

2001 if exceptions: 

2002 raise MultipleParseExceptions(exceptions) 

2003 

2004 def parse_key_and_set_value(self, key: str, value: str) -> None: 

2005 ''' 

2006 Find the corresponding :class:`~confattr.config.Config` instance for :paramref:`~confattr.configfile.Set.parse_key_and_set_value.key` and call :meth:`~confattr.configfile.Set.set_value` with the return value of :meth:`config_file.parse_value() <confattr.configfile.ConfigFile.parse_value>`. 

2007 

2008 :raises ParseException: if key is invalid or if :meth:`config_file.parse_value() <confattr.configfile.ConfigFile.parse_value>` or :meth:`~confattr.configfile.Set.set_value` raises a :class:`ValueError` 

2009 ''' 

2010 if key not in self.config_file.config_instances: 

2011 raise ParseException(f'invalid key {key!r}') 

2012 

2013 instance = self.config_file.config_instances[key] 

2014 try: 

2015 self.set_value(instance, self.config_file.parse_value(instance, value, raw=self.raw)) 

2016 except ValueError as e: 

2017 raise ParseException(str(e)) 

2018 

2019 def set_value(self, instance: 'Config[T2]', value: 'T2') -> None: 

2020 ''' 

2021 Assign :paramref:`~confattr.configfile.Set.set_value.value` to :paramref`instance` by calling :meth:`Config.set_value() <confattr.config.Config.set_value>` with :attr:`ConfigFile.config_id <confattr.configfile.ConfigFile.config_id>` of :attr:`~confattr.configfile.Set.config_file`. 

2022 Afterwards call :meth:`UiNotifier.show_info() <confattr.configfile.UiNotifier.show_info>`. 

2023 ''' 

2024 instance.set_value(self.config_file.config_id, value) 

2025 self.ui_notifier.show_info(f'set {instance.key} to {self.config_file.format_value(instance, self.config_file.config_id)}') 

2026 

2027 

2028 # ------- save ------- 

2029 

2030 def iter_config_instances_to_be_saved(self, **kw: 'Unpack[SaveKwargs]') -> 'Iterator[Config[object]]': 

2031 ''' 

2032 :param config_instances: The settings to consider 

2033 :param ignore: Skip these settings 

2034 

2035 Iterate over all given :paramref:`~confattr.configfile.Set.iter_config_instances_to_be_saved.config_instances` and expand all :class:`~confattr.config.DictConfig` instances into the :class:`~confattr.config.Config` instances they consist of. 

2036 Sort the resulting list if :paramref:`~confattr.configfile.Set.iter_config_instances_to_be_saved.config_instances` is not a :class:`list` or a :class:`tuple`. 

2037 Yield all :class:`~confattr.config.Config` instances which are not (directly or indirectly) contained in :paramref:`~confattr.configfile.Set.iter_config_instances_to_be_saved.ignore` and where :meth:`Config.wants_to_be_exported() <confattr.config.Config.wants_to_be_exported>` returns true. 

2038 ''' 

2039 config_instances = kw['config_instances'] 

2040 ignore = kw['ignore'] 

2041 

2042 config_keys = [] 

2043 for c in config_instances: 

2044 if isinstance(c, DictConfig): 

2045 config_keys.extend(sorted(c.iter_keys())) 

2046 else: 

2047 config_keys.append(c.key) 

2048 if not isinstance(config_instances, (list, tuple)): 

2049 config_keys = sorted(config_keys) 

2050 

2051 if ignore is not None: 

2052 tmp = set() 

2053 for c in tuple(ignore): 

2054 if isinstance(c, DictConfig): 

2055 tmp |= set(c._values.values()) 

2056 else: 

2057 tmp.add(c) 

2058 ignore = tmp 

2059 

2060 for key in config_keys: 

2061 instance = self.config_file.config_instances[key] 

2062 if not instance.wants_to_be_exported(): 

2063 continue 

2064 

2065 if ignore is not None and instance in ignore: 

2066 continue 

2067 

2068 yield instance 

2069 

2070 

2071 #: A temporary variable used in :meth:`~confattr.configfile.Set.write_config_help` to prevent repeating the help of several :class:`~confattr.config.Config` instances belonging to the same :class:`~confattr.config.DictConfig`. It is reset in :meth:`~confattr.configfile.Set.save`. 

2072 last_name: 'str|None' 

2073 

2074 def save(self, writer: FormattedWriter, **kw: 'Unpack[SaveKwargs]') -> None: 

2075 ''' 

2076 :param writer: The file to write to 

2077 :param bool no_multi: If true: treat :class:`~confattr.config.MultiConfig` instances like normal :class:`~confattr.config.Config` instances and only write their default value. If false: Separate :class:`~confattr.config.MultiConfig` instances and print them once for every :attr:`MultiConfig.config_ids <confattr.config.MultiConfig.config_ids>`. 

2078 :param bool comments: If false: don't write help for data types 

2079 

2080 Iterate over all :class:`~confattr.config.Config` instances with :meth:`~confattr.configfile.Set.iter_config_instances_to_be_saved`, 

2081 split them into normal :class:`~confattr.config.Config` and :class:`~confattr.config.MultiConfig` and write them with :meth:`~confattr.configfile.Set.save_config_instance`. 

2082 But before that set :attr:`~confattr.configfile.Set.last_name` to None (which is used by :meth:`~confattr.configfile.Set.write_config_help`) 

2083 and write help for data types based on :meth:`~confattr.configfile.Set.get_help_for_data_types`. 

2084 ''' 

2085 no_multi = kw['no_multi'] 

2086 comments = kw['comments'] 

2087 

2088 config_instances = list(self.iter_config_instances_to_be_saved(**kw)) 

2089 normal_configs = [] 

2090 multi_configs = [] 

2091 if no_multi: 

2092 normal_configs = config_instances 

2093 else: 

2094 for instance in config_instances: 

2095 if isinstance(instance, MultiConfig): 

2096 multi_configs.append(instance) 

2097 else: 

2098 normal_configs.append(instance) 

2099 

2100 self.last_name: 'str|None' = None 

2101 

2102 if normal_configs: 

2103 if multi_configs: 

2104 writer.write_heading(SectionLevel.SECTION, 'Application wide settings') 

2105 elif self.should_write_heading: 

2106 writer.write_heading(SectionLevel.SECTION, 'Settings') 

2107 

2108 if comments: 

2109 type_help = self.get_help_for_data_types(normal_configs) 

2110 if type_help: 

2111 writer.write_heading(SectionLevel.SUB_SECTION, 'Data types') 

2112 writer.write_lines(type_help) 

2113 

2114 for instance in normal_configs: 

2115 self.save_config_instance(writer, instance, config_id=None, **kw) 

2116 

2117 if multi_configs: 

2118 if normal_configs: 

2119 writer.write_heading(SectionLevel.SECTION, 'Settings which can have different values for different objects') 

2120 elif self.should_write_heading: 

2121 writer.write_heading(SectionLevel.SECTION, 'Settings') 

2122 

2123 if comments: 

2124 type_help = self.get_help_for_data_types(multi_configs) 

2125 if type_help: 

2126 writer.write_heading(SectionLevel.SUB_SECTION, 'Data types') 

2127 writer.write_lines(type_help) 

2128 

2129 for instance in multi_configs: 

2130 self.save_config_instance(writer, instance, config_id=instance.default_config_id, **kw) 

2131 

2132 for config_id in MultiConfig.config_ids: 

2133 writer.write_line('') 

2134 self.config_file.write_config_id(writer, config_id) 

2135 for instance in multi_configs: 

2136 self.save_config_instance(writer, instance, config_id, **kw) 

2137 

2138 def save_config_instance(self, writer: FormattedWriter, instance: 'Config[object]', config_id: 'ConfigId|None', **kw: 'Unpack[SaveKwargs]') -> None: 

2139 ''' 

2140 :param writer: The file to write to 

2141 :param instance: The config value to be saved 

2142 :param config_id: Which value to be written in case of a :class:`~confattr.config.MultiConfig`, should be :obj:`None` for a normal :class:`~confattr.config.Config` instance 

2143 :param bool comments: If true: call :meth:`~confattr.configfile.Set.write_config_help` 

2144 

2145 Convert the :class:`~confattr.config.Config` instance into a value str with :meth:`config_file.format_value() <confattr.configfile.ConfigFile.format_value>`, 

2146 wrap it in quotes if necessary with :meth:`config_file.quote() <confattr.configfile.ConfigFile.quote>` and write it to :paramref:`~confattr.configfile.Set.save_config_instance.writer`. 

2147 ''' 

2148 if kw['comments']: 

2149 self.write_config_help(writer, instance) 

2150 value = self.config_file.format_value(instance, config_id) 

2151 value = self.config_file.quote(value) 

2152 ln = f'{self.get_name()} {instance.key} = {value}' 

2153 writer.write_command(ln) 

2154 

2155 def write_config_help(self, writer: FormattedWriter, instance: Config[typing.Any], *, group_dict_configs: bool = True) -> None: 

2156 ''' 

2157 :param writer: The output to write to 

2158 :param instance: The config value to be saved 

2159 

2160 Write a comment which explains the meaning and usage of this setting 

2161 based on :meth:`instance.type.get_description() <confattr.formatters.AbstractFormatter.get_description>` and :attr:`Config.help <confattr.config.Config.help>`. 

2162 

2163 Use :attr:`~confattr.configfile.Set.last_name` to write the help only once for all :class:`~confattr.config.Config` instances belonging to the same :class:`~confattr.config.DictConfig` instance. 

2164 ''' 

2165 if group_dict_configs and instance.parent is not None: 

2166 name = instance.parent.key_prefix 

2167 else: 

2168 name = instance.key 

2169 if name == self.last_name: 

2170 return 

2171 

2172 formatter = HelpFormatterWrapper(self.config_file.formatter_class) 

2173 writer.write_heading(SectionLevel.SUB_SECTION, name) 

2174 writer.write_lines(formatter.format_text(instance.type.get_description(self.config_file)).rstrip()) 

2175 #if instance.unit: 

2176 # writer.write_line('unit: %s' % instance.unit) 

2177 if isinstance(instance.help, dict): 

2178 for key, val in instance.help.items(): 

2179 key_name = self.config_file.format_any_value(instance.type.get_primitives()[-1], key) 

2180 val = inspect.cleandoc(val) 

2181 writer.write_lines(formatter.format_item(bullet=key_name+': ', text=val).rstrip()) 

2182 elif isinstance(instance.help, str): 

2183 writer.write_lines(formatter.format_text(inspect.cleandoc(instance.help)).rstrip()) 

2184 

2185 self.last_name = name 

2186 

2187 

2188 def get_data_type_name_to_help_map(self, config_instances: 'Iterable[Config[object]]') -> 'dict[str, str]': 

2189 ''' 

2190 :param config_instances: All config values to be saved 

2191 :return: A dictionary containing the type names as keys and the help as values 

2192 

2193 The returned dictionary contains the help for all data types except enumerations 

2194 which occur in :paramref:`~confattr.configfile.Set.get_data_type_name_to_help_map.config_instances`. 

2195 The help is gathered from the :attr:`~confattr.configfile.Set.help` attribute of the type 

2196 or :meth:`Primitive.get_help() <confattr.formatters.Primitive.get_help>`. 

2197 The help is cleaned up with :func:`inspect.cleandoc`. 

2198 ''' 

2199 help_text: 'dict[str, str]' = {} 

2200 for instance in config_instances: 

2201 for t in instance.type.get_primitives(): 

2202 name = t.get_type_name() 

2203 if name in help_text: 

2204 continue 

2205 

2206 h = t.get_help(self.config_file) 

2207 if not h: 

2208 continue 

2209 help_text[name] = inspect.cleandoc(h) 

2210 

2211 return help_text 

2212 

2213 def add_help_for_data_types(self, formatter: HelpFormatterWrapper, config_instances: 'Iterable[Config[object]]') -> None: 

2214 help_map = self.get_data_type_name_to_help_map(config_instances) 

2215 if not help_map: 

2216 return 

2217 

2218 for name in sorted(help_map.keys()): 

2219 formatter.add_start_section(name) 

2220 formatter.add_text(help_map[name]) 

2221 formatter.add_end_section() 

2222 

2223 def get_help_for_data_types(self, config_instances: 'Iterable[Config[object]]') -> str: 

2224 formatter = self.create_formatter() 

2225 self.add_help_for_data_types(formatter, config_instances) 

2226 return formatter.format_help().rstrip('\n') 

2227 

2228 # ------- help ------- 

2229 

2230 def add_help_to(self, formatter: HelpFormatterWrapper) -> None: 

2231 super().add_help_to(formatter) 

2232 

2233 kw: 'SaveKwargs' = {} 

2234 self.config_file.set_save_default_arguments(kw) 

2235 config_instances = list(self.iter_config_instances_to_be_saved(**kw)) 

2236 self.last_name = None 

2237 

2238 formatter.add_start_section('data types') 

2239 self.add_help_for_data_types(formatter, config_instances) 

2240 formatter.add_end_section() 

2241 

2242 if self.config_file.enable_config_ids: 

2243 normal_configs = [] 

2244 multi_configs = [] 

2245 for instance in config_instances: 

2246 if isinstance(instance, MultiConfig): 

2247 multi_configs.append(instance) 

2248 else: 

2249 normal_configs.append(instance) 

2250 else: 

2251 normal_configs = config_instances 

2252 multi_configs = [] 

2253 

2254 if normal_configs: 

2255 if self.config_file.enable_config_ids: 

2256 formatter.add_start_section('application wide settings') 

2257 else: 

2258 formatter.add_start_section('settings') 

2259 for instance in normal_configs: 

2260 self.add_config_help(formatter, instance) 

2261 formatter.add_end_section() 

2262 

2263 if multi_configs: 

2264 formatter.add_start_section('settings which can have different values for different objects') 

2265 formatter.add_text(inspect.cleandoc(self.config_file.get_help_config_id())) 

2266 for instance in multi_configs: 

2267 self.add_config_help(formatter, instance) 

2268 formatter.add_end_section() 

2269 

2270 def add_config_help(self, formatter: HelpFormatterWrapper, instance: Config[typing.Any]) -> None: 

2271 formatter.add_start_section(instance.key) 

2272 formatter.add_text(instance.type.get_description(self.config_file)) 

2273 if isinstance(instance.help, dict): 

2274 for key, val in instance.help.items(): 

2275 key_name = self.config_file.format_any_value(instance.type.get_primitives()[-1], key) 

2276 val = inspect.cleandoc(val) 

2277 formatter.add_item(bullet=key_name+': ', text=val) 

2278 elif isinstance(instance.help, str): 

2279 formatter.add_text(inspect.cleandoc(instance.help)) 

2280 formatter.add_end_section() 

2281 

2282 # ------- auto complete ------- 

2283 

2284 def get_completions(self, cmd: 'Sequence[str]', argument_pos: int, cursor_pos: int, *, in_between: bool, start_of_line: str, end_of_line: str) -> 'tuple[str, list[str], str]': 

2285 if argument_pos >= len(cmd): 

2286 start = '' 

2287 else: 

2288 start = cmd[argument_pos][:cursor_pos] 

2289 

2290 if len(cmd) <= 1: 

2291 return self.get_completions_for_key(start, start_of_line=start_of_line, end_of_line=end_of_line) 

2292 elif self.is_vim_style(cmd): 

2293 return self.get_completions_for_vim_style_arg(cmd, argument_pos, start, start_of_line=start_of_line, end_of_line=end_of_line) 

2294 else: 

2295 return self.get_completions_for_ranger_style_arg(cmd, argument_pos, start, start_of_line=start_of_line, end_of_line=end_of_line) 

2296 

2297 def get_completions_for_vim_style_arg(self, cmd: 'Sequence[str]', argument_pos: int, start: str, *, start_of_line: str, end_of_line: str) -> 'tuple[str, list[str], str]': 

2298 if self.KEY_VAL_SEP in start: 

2299 key, start = start.split(self.KEY_VAL_SEP, 1) 

2300 start_of_line += key + self.KEY_VAL_SEP 

2301 return self.get_completions_for_value(key, start, start_of_line=start_of_line, end_of_line=end_of_line) 

2302 else: 

2303 return self.get_completions_for_key(start, start_of_line=start_of_line, end_of_line=end_of_line) 

2304 

2305 def get_completions_for_ranger_style_arg(self, cmd: 'Sequence[str]', argument_pos: int, start: str, *, start_of_line: str, end_of_line: str) -> 'tuple[str, list[str], str]': 

2306 if argument_pos == 1: 

2307 return self.get_completions_for_key(start, start_of_line=start_of_line, end_of_line=end_of_line) 

2308 elif argument_pos == 2 or (argument_pos == 3 and cmd[2] == self.KEY_VAL_SEP): 

2309 return self.get_completions_for_value(cmd[1], start, start_of_line=start_of_line, end_of_line=end_of_line) 

2310 else: 

2311 return start_of_line, [], end_of_line 

2312 

2313 def get_completions_for_key(self, start: str, *, start_of_line: str, end_of_line: str) -> 'tuple[str, list[str], str]': 

2314 completions = [key for key in self.config_file.config_instances.keys() if key.startswith(start)] 

2315 return start_of_line, completions, end_of_line 

2316 

2317 def get_completions_for_value(self, key: str, start: str, *, start_of_line: str, end_of_line: str) -> 'tuple[str, list[str], str]': 

2318 applicable, start_of_line, completions, end_of_line = self.config_file.get_completions_for_expand(start, start_of_line=start_of_line, end_of_line=end_of_line) 

2319 if applicable: 

2320 return start_of_line, completions, end_of_line 

2321 

2322 instance = self.config_file.config_instances.get(key) 

2323 if instance is None: 

2324 return start_of_line, [], end_of_line 

2325 

2326 return instance.type.get_completions(self.config_file, start_of_line, start, end_of_line) 

2327 

2328 

2329class Include(ConfigFileArgparseCommand): 

2330 

2331 ''' 

2332 Load another config file. 

2333 

2334 This is useful if a config file is getting so big that you want to split it up 

2335 or if you want to have different config files for different use cases which all include the same standard config file to avoid redundancy 

2336 or if you want to bind several commands to one key which executes one command with ConfigFile.parse_line(). 

2337 ''' 

2338 

2339 help_config_id = ''' 

2340 By default the loaded config file starts with which ever config id is currently active. 

2341 This is useful if you want to use the same values for several config ids: 

2342 Write the set commands without a config id to a separate config file and include this file for every config id where these settings shall apply. 

2343 

2344 After the include the config id is reset to the config id which was active at the beginning of the include 

2345 because otherwise it might lead to confusion if the config id is changed in the included config file. 

2346 ''' 

2347 

2348 def init_parser(self, parser: ArgumentParser) -> None: 

2349 parser.add_argument('path', help='The config file to load. Slashes are replaced with the directory separator appropriate for the current operating system. If the path contains a space it must be wrapped in single or double quotes.') 

2350 if self.config_file.enable_config_ids: 

2351 assert parser.description is not None 

2352 parser.description += '\n\n' + inspect.cleandoc(self.help_config_id) 

2353 group = parser.add_mutually_exclusive_group() 

2354 group.add_argument('--reset-config-id-before', action='store_true', help='Ignore any config id which might be active when starting the include') 

2355 group.add_argument('--no-reset-config-id-after', action='store_true', help='Treat the included lines as if they were written in the same config file instead of the include command') 

2356 

2357 self.nested_includes: 'list[str]' = [] 

2358 

2359 def run_parsed(self, args: argparse.Namespace) -> None: 

2360 fn_imp = args.path 

2361 fn_imp = fn_imp.replace('/', os.path.sep) 

2362 fn_imp = os.path.expanduser(fn_imp) 

2363 if not os.path.isabs(fn_imp): 

2364 fn = self.config_file.context_file_name 

2365 if fn is None: 

2366 fn = self.config_file.get_save_path() 

2367 fn_imp = os.path.join(os.path.dirname(os.path.abspath(fn)), fn_imp) 

2368 

2369 if fn_imp in self.nested_includes: 

2370 raise ParseException(f'circular include of file {fn_imp!r}') 

2371 if not os.path.isfile(fn_imp): 

2372 raise ParseException(f'no such file {fn_imp!r}') 

2373 

2374 self.nested_includes.append(fn_imp) 

2375 

2376 if self.config_file.enable_config_ids and args.no_reset_config_id_after: 

2377 self.config_file.load_without_resetting_config_id(fn_imp) 

2378 elif self.config_file.enable_config_ids and args.reset_config_id_before: 

2379 config_id = self.config_file.config_id 

2380 self.config_file.load_file(fn_imp) 

2381 self.config_file.config_id = config_id 

2382 else: 

2383 config_id = self.config_file.config_id 

2384 self.config_file.load_without_resetting_config_id(fn_imp) 

2385 self.config_file.config_id = config_id 

2386 

2387 assert self.nested_includes[-1] == fn_imp 

2388 del self.nested_includes[-1] 

2389 

2390 def get_completions_for_action(self, action: 'argparse.Action|None', start: str, *, start_of_line: str, end_of_line: str) -> 'tuple[str, list[str], str]': 

2391 # action does not have a name and metavar is None if not explicitly set, dest is the only way to identify the action 

2392 if action is not None and action.dest == 'path': 

2393 return self.config_file.get_completions_for_file_name(start, relative_to=os.path.dirname(self.config_file.get_save_path()), start_of_line=start_of_line, end_of_line=end_of_line) 

2394 return super().get_completions_for_action(action, start, start_of_line=start_of_line, end_of_line=end_of_line) 

2395 

2396 

2397class Echo(ConfigFileArgparseCommand): 

2398 

2399 ''' 

2400 Display a message. 

2401 

2402 Settings and environment variables are expanded like in the value of a set command. 

2403 ''' 

2404 

2405 def init_parser(self, parser: ArgumentParser) -> None: 

2406 parser.add_argument('-l', '--level', default=NotificationLevel.INFO, type=NotificationLevel, metavar='{%s}' % ','.join(l.value for l in NotificationLevel), help="The notification level may influence the formatting but messages printed with echo are always displayed regardless of the notification level.") 

2407 parser.add_argument('-r', '--raw', action='store_true', help="Do not expand settings and environment variables.") 

2408 parser.add_argument('msg', nargs=argparse.ONE_OR_MORE, help="The message to display") 

2409 

2410 def run_parsed(self, args: argparse.Namespace) -> None: 

2411 msg = ' '.join(self.config_file.expand(m) for m in args.msg) 

2412 self.ui_notifier.show(args.level, msg, ignore_filter=True) 

2413 

2414 

2415 def get_completions(self, cmd: 'Sequence[str]', argument_pos: int, cursor_pos: int, *, in_between: bool, start_of_line: str, end_of_line: str) -> 'tuple[str, list[str], str]': 

2416 if argument_pos >= len(cmd): 

2417 start = '' 

2418 else: 

2419 start = cmd[argument_pos][:cursor_pos] 

2420 

2421 applicable, start_of_line, completions, end_of_line = self.config_file.get_completions_for_expand(start, start_of_line=start_of_line, end_of_line=end_of_line) 

2422 return start_of_line, completions, end_of_line 

2423 

2424class Help(ConfigFileArgparseCommand): 

2425 

2426 ''' 

2427 Display help. 

2428 ''' 

2429 

2430 def init_parser(self, parser: ArgumentParser) -> None: 

2431 parser.add_argument('cmd', nargs='?', help="The command for which you want help") 

2432 

2433 def run_parsed(self, args: argparse.Namespace) -> None: 

2434 if args.cmd: 

2435 if args.cmd not in self.config_file.command_dict: 

2436 raise ParseException(f"unknown command {args.cmd!r}") 

2437 cmd = self.config_file.command_dict[args.cmd] 

2438 out = cmd.get_help() 

2439 else: 

2440 out = "The following commands are defined:" 

2441 for cmd in self.config_file.commands: 

2442 out += "\n- %s" % "/".join(cmd.get_names()) 

2443 

2444 out += "\n" 

2445 out += "\nUse `help <cmd>` to get more information about a command." 

2446 

2447 self.ui_notifier.show(NotificationLevel.INFO, out, ignore_filter=True, no_context=True) 

2448 

2449 def get_completions_for_action(self, action: 'argparse.Action|None', start: str, *, start_of_line: str, end_of_line: str) -> 'tuple[str, list[str], str]': 

2450 if action and action.dest == 'cmd': 

2451 start_of_line, completions, end_of_line = self.config_file.get_completions_command_name(start, cursor_pos=len(start), start_of_line=start_of_line, end_of_line=end_of_line) 

2452 return start_of_line, completions, end_of_line 

2453 

2454 return super().get_completions_for_action(action, start, start_of_line=start_of_line, end_of_line=end_of_line) 

2455 

2456 

2457class UnknownCommand(ConfigFileCommand, abstract=True): 

2458 

2459 name = DEFAULT_COMMAND 

2460 

2461 def run(self, cmd: 'Sequence[str]') -> None: 

2462 raise ParseException('unknown command %r' % cmd[0])