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

1365 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-03 07:55 +0100

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 textwrap 

15import functools 

16import inspect 

17import io 

18import warnings 

19import abc 

20import typing 

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

22 

23import appdirs 

24 

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

26from .formatters import AbstractFormatter 

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

28from . import state 

29 

30if typing.TYPE_CHECKING: 

31 from typing_extensions import Unpack 

32 

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

34T2 = typing.TypeVar('T2') 

35 

36 

37#: 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. 

38DEFAULT_COMMAND = '' 

39 

40 

41if hasattr(typing, 'Protocol'): 

42 class PathType(typing.Protocol): 

43 

44 def __init__(self, path: str) -> None: 

45 ... 

46 

47 def expand(self) -> str: 

48 ... 

49 

50 

51# ---------- UI notifier ---------- 

52 

53@functools.total_ordering 

54class NotificationLevel: 

55 

56 ''' 

57 Instances of this class indicate how important a message is. 

58 

59 I am not using an enum anymore in order to allow users to add custom levels. 

60 Like an enum, however, ``NotificationLevel('error')`` returns the existing instance instead of creating a new one. 

61 In order to create a new instance use :meth:`~confattr.configfile.NotificationLevel.new`. 

62 ''' 

63 

64 INFO: 'NotificationLevel' 

65 ERROR: 'NotificationLevel' 

66 

67 _instances: 'list[NotificationLevel]' = [] 

68 

69 def __new__(cls, value: str, *, new: bool = False, more_important_than: 'NotificationLevel|None' = None, less_important_than: 'NotificationLevel|None' = None) -> 'NotificationLevel': 

70 ''' 

71 :return: An existing instance (see :meth:`~confattr.configfile.NotificationLevel.get`) or a new instance if :paramref:`~confattr.configfile.NotificationLevel.new` is true (see :meth:`~confattr.configfile.NotificationLevel.new`) 

72 :param value: The name of the notification level 

73 :param new: If false: return an existing instance with :meth:`~confattr.configfile.NotificationLevel.get`. If true: create a new instance. 

74 :param more_important_than: If :paramref:`~confattr.configfile.NotificationLevel.new` is true either this or :paramref:`~confattr.configfile.NotificationLevel.less_important_than` must be given. 

75 :param less_important_than: If :paramref:`~confattr.configfile.NotificationLevel.new` is true either this or :paramref:`~confattr.configfile.NotificationLevel.more_important_than` must be given. 

76 ''' 

77 if new: 

78 if more_important_than and less_important_than: 

79 raise TypeError("more_important_than and less_important_than are mutually exclusive, you can only pass one of them") 

80 elif cls._instances and not (more_important_than or less_important_than): 

81 raise TypeError(f"you must specify how important {value!r} is by passing either more_important_than or less_important_than") 

82 

83 try: 

84 out = cls.get(value) 

85 except ValueError: 

86 pass 

87 else: 

88 if more_important_than and out < more_important_than: 

89 raise ValueError(f"{out} is already defined and it's less important than {more_important_than}") 

90 elif less_important_than and out > less_important_than: 

91 raise ValueError(f"{out} is already defined and it's more important than {less_important_than}") 

92 warnings.warn(f"{out!r} is already defined, ignoring", stacklevel=3) 

93 return out 

94 

95 return super().__new__(cls) 

96 

97 if more_important_than: 

98 raise TypeError('more_important_than must not be passed when new = False') 

99 if less_important_than: 

100 raise TypeError('less_important_than must not be passed when new = False') 

101 

102 return cls.get(value) 

103 

104 def __init__(self, value: str, *, new: bool = False, more_important_than: 'NotificationLevel|None' = None, less_important_than: 'NotificationLevel|None' = None) -> None: 

105 if hasattr(self, '_initialized'): 

106 # __init__ is called every time, even if __new__ has returned an old object 

107 return 

108 

109 assert new 

110 self._initialized = True 

111 self.value = value 

112 

113 if more_important_than: 

114 i = self._instances.index(more_important_than) + 1 

115 elif less_important_than: 

116 i = self._instances.index(less_important_than) 

117 elif not self._instances: 

118 i = 0 

119 else: 

120 assert False 

121 

122 self._instances.insert(i, self) 

123 

124 @classmethod 

125 def new(cls, value: str, *, more_important_than: 'NotificationLevel|None' = None, less_important_than: 'NotificationLevel|None' = None) -> 'NotificationLevel': 

126 ''' 

127 :param value: A name for the new notification level 

128 :param more_important_than: Specify the importance of the new notification level. Either this or :paramref:`~confattr.configfile.NotificationLevel.new.less_important_than` must be given but not both. 

129 :param less_important_than: Specify the importance of the new notification level. Either this or :paramref:`~confattr.configfile.NotificationLevel.new.more_important_than` must be given but not both. 

130 ''' 

131 return cls(value, more_important_than=more_important_than, less_important_than=less_important_than, new=True) 

132 

133 @classmethod 

134 def get(cls, value: str) -> 'NotificationLevel': 

135 ''' 

136 :return: The instance of this class for the given value 

137 :raises ValueError: If there is no instance for the given value 

138 ''' 

139 for lvl in cls._instances: 

140 if lvl.value == value: 

141 return lvl 

142 

143 raise ValueError('') 

144 

145 @classmethod 

146 def get_instances(cls) -> 'Sequence[NotificationLevel]': 

147 ''' 

148 :return: A sequence of all instances of this class 

149 ''' 

150 return cls._instances 

151 

152 def __lt__(self, other: typing.Any) -> bool: 

153 if self.__class__ is other.__class__: 

154 return self._instances.index(self) < self._instances.index(other) 

155 return NotImplemented 

156 

157 def __str__(self) -> str: 

158 return self.value 

159 

160 def __repr__(self) -> str: 

161 return "%s(%r)" % (type(self).__name__, self.value) 

162 

163 

164NotificationLevel.INFO = NotificationLevel.new('info') 

165NotificationLevel.ERROR = NotificationLevel.new('error', more_important_than=NotificationLevel.INFO) 

166 

167 

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

169 

170class Message: 

171 

172 ''' 

173 A message which should be displayed to the user. 

174 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>`. 

175 

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

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

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

179 ''' 

180 

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

182 ENVIRONMENT_VARIABLES = 'environment variables' 

183 

184 

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

186 

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

188 #: :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. 

189 notification_level: NotificationLevel 

190 

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

192 message: 'str|BaseException' 

193 

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

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

196 #: 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. 

197 file_name: 'str|None' 

198 

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

200 line_number: 'int|None' 

201 

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

203 line: str 

204 

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

206 no_context: bool 

207 

208 

209 _last_file_name: 'str|None' = None 

210 

211 @classmethod 

212 def reset(cls) -> None: 

213 ''' 

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

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

216 ''' 

217 cls._last_file_name = None 

218 

219 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: 

220 self.notification_level = notification_level 

221 self.message = message 

222 self.file_name = file_name 

223 self.line_number = line_number 

224 self.line = line 

225 self.no_context = no_context 

226 

227 @property 

228 def lvl(self) -> NotificationLevel: 

229 ''' 

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

231 ''' 

232 return self.notification_level 

233 

234 def format_msg_line(self) -> str: 

235 ''' 

236 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. 

237 ''' 

238 msg = str(self.message) 

239 if self.line and not self.no_context: 

240 if self.line_number is not None: 

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

242 else: 

243 lnref = 'line' 

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

245 

246 return msg 

247 

248 def format_file_name(self) -> str: 

249 ''' 

250 :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 

251 ''' 

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

253 if file_name == self._last_file_name: 

254 return '' 

255 

256 if file_name: 

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

258 else: 

259 out = '' 

260 

261 if self._last_file_name is not None: 

262 out = '\n' + out 

263 

264 type(self)._last_file_name = file_name 

265 

266 return out 

267 

268 

269 def format_file_name_msg_line(self) -> str: 

270 ''' 

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

272 ''' 

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

274 

275 

276 def __str__(self) -> str: 

277 ''' 

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

279 ''' 

280 return self.format_file_name_msg_line() 

281 

282 def __repr__(self) -> str: 

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

284 

285 @staticmethod 

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

287 return repr(obj) 

288 

289 

290class UiNotifier: 

291 

292 ''' 

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

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

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

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

297 

298 This object can also filter the messages. 

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

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

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

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

303 ''' 

304 

305 # ------- public methods ------- 

306 

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

308 ''' 

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

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

311 ''' 

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

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

314 self._notification_level = notification_level 

315 self._config_file = config_file 

316 

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

318 ''' 

319 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. 

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

321 ''' 

322 self._callback = callback 

323 

324 for msg in self._messages: 

325 callback(msg) 

326 self._messages.clear() 

327 

328 

329 @property 

330 def notification_level(self) -> NotificationLevel: 

331 ''' 

332 Ignore messages that are less important than this level. 

333 ''' 

334 if isinstance(self._notification_level, Config): 

335 return self._notification_level.value 

336 else: 

337 return self._notification_level 

338 

339 @notification_level.setter 

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

341 if isinstance(self._notification_level, Config): 

342 self._notification_level.value = val 

343 else: 

344 self._notification_level = val 

345 

346 

347 # ------- called by ConfigFile ------- 

348 

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

350 ''' 

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

352 ''' 

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

354 

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

356 ''' 

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

358 ''' 

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

360 

361 

362 # ------- internal methods ------- 

363 

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

365 ''' 

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

367 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. 

368 

369 :param notification_level: The importance of the message 

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

371 :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>`. 

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

373 ''' 

374 if notification_level < self.notification_level and not ignore_filter: 

375 return 

376 

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

378 no_context = True 

379 

380 message = Message( 

381 notification_level = notification_level, 

382 message = msg, 

383 file_name = self._config_file.context_file_name if self._config_file else None, 

384 line_number = self._config_file.context_line_number if self._config_file else None, 

385 line = self._config_file.context_line if self._config_file else '', 

386 no_context = no_context, 

387 ) 

388 

389 if self._callback: 

390 self._callback(message) 

391 else: 

392 self._messages.append(message) 

393 

394 

395# ---------- format help ---------- 

396 

397class SectionLevel(SortedEnum): 

398 

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

400 SECTION = 'section' 

401 

402 #: 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 

403 SUB_SECTION = 'sub-section' 

404 

405 

406class FormattedWriter(abc.ABC): 

407 

408 @abc.abstractmethod 

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

410 ''' 

411 Write a single line of documentation. 

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

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

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

415 ''' 

416 pass 

417 

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

419 ''' 

420 Write one or more lines of documentation. 

421 ''' 

422 for ln in text.splitlines(): 

423 self.write_line(ln) 

424 

425 @abc.abstractmethod 

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

427 ''' 

428 Write a heading. 

429 

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

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

432 in order to keep the line wrapping consistent. 

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

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

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

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

437 to understand the help generated by argparse 

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

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

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

441 

442 :param lvl: How to format the heading 

443 :param heading: The heading 

444 ''' 

445 pass 

446 

447 @abc.abstractmethod 

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

449 ''' 

450 Write a config file command. 

451 ''' 

452 pass 

453 

454 

455class TextIOWriter(FormattedWriter): 

456 

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

458 self.f = f 

459 self.ignore_empty_lines = True 

460 

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

462 if self.ignore_empty_lines and not line: 

463 return 

464 

465 print(line, file=self.f) 

466 self.ignore_empty_lines = False 

467 

468 

469class ConfigFileWriter(TextIOWriter): 

470 

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

472 super().__init__(f) 

473 self.prefix = prefix 

474 

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

476 self.write_line_raw(cmd) 

477 

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

479 if line: 

480 line = self.prefix + line 

481 

482 self.write_line_raw(line) 

483 

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

485 if lvl is SectionLevel.SECTION: 

486 self.write_line('') 

487 self.write_line('') 

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

489 self.write_line(heading) 

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

491 else: 

492 self.write_line('') 

493 self.write_line(heading) 

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

495 

496class HelpWriter(TextIOWriter): 

497 

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

499 self.write_line_raw(line) 

500 

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

502 self.write_line('') 

503 if lvl is SectionLevel.SECTION: 

504 self.write_line(heading) 

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

506 else: 

507 self.write_line(heading) 

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

509 

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

511 pass # pragma: no cover 

512 

513 

514# ---------- internal exceptions ---------- 

515 

516class ParseException(Exception): 

517 

518 ''' 

519 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. 

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

521 ''' 

522 

523class MultipleParseExceptions(Exception): 

524 

525 ''' 

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

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

528 ''' 

529 

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

531 super().__init__() 

532 self.exceptions = exceptions 

533 

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

535 return iter(self.exceptions) 

536 

537 

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

539 

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

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

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

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

544 no_multi: bool 

545 comments: bool 

546 commands: 'Sequence[type[ConfigFileCommand]|abc.ABCMeta]' 

547 ignore_commands: 'Sequence[type[ConfigFileCommand]|abc.ABCMeta]' 

548 

549 

550# ---------- ConfigFile class ---------- 

551 

552class ArgPos: 

553 ''' 

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

555 ''' 

556 

557 #: 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. 

558 argument_pos: int 

559 

560 #: 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. 

561 in_between: bool 

562 

563 #: 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 

564 i0: int 

565 

566 #: 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 

567 i1: int 

568 

569 

570class ConfigFile: 

571 

572 ''' 

573 Read or write a config file. 

574 

575 All :class:`~confattr.config.Config` objects must be instantiated before instantiating this class. 

576 ''' 

577 

578 COMMENT = '#' 

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

580 ENTER_GROUP_PREFIX = '[' 

581 ENTER_GROUP_SUFFIX = ']' 

582 

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

584 ITEM_SEP = ',' 

585 

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

587 KEY_SEP = ':' 

588 

589 

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

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

592 

593 #: 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`. 

594 config_id: 'ConfigId|None' 

595 

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

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

598 #: 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.) 

599 config_path: 'str|None' = None 

600 

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

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

603 #: 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.) 

604 config_directory: 'str|None' = None 

605 

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

607 #: 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.). 

608 config_name = 'config' 

609 

610 #: 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`. 

611 env_variables: 'list[str]' 

612 

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

614 #: 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. 

615 envprefix: str 

616 

617 #: 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). 

618 context_file_name: 'str|None' = None 

619 #: 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. 

620 context_line_number: 'int|None' = None 

621 #: The line which is currently parsed. 

622 context_line: str = '' 

623 

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

625 #: 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) 

626 enable_config_ids: bool 

627 

628 

629 #: 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*. 

630 command_dict: 'dict[str, ConfigFileCommand]' 

631 

632 #: 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. 

633 commands: 'list[ConfigFileCommand]' 

634 

635 

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

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

638 

639 #: 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. 

640 show_line_always: bool 

641 

642 

643 def __init__(self, *, 

644 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 

645 appname: str, 

646 authorname: 'str|None' = None, 

647 config_instances: 'Iterable[Config[typing.Any] | DictConfig[typing.Any, typing.Any]]|None' = None, 

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

649 commands: 'Iterable[type[ConfigFileCommand]|abc.ABCMeta]|None' = None, 

650 ignore_commands: 'Sequence[type[ConfigFileCommand]|abc.ABCMeta]|None' = None, 

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

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

653 enable_config_ids: 'bool|None' = None, 

654 show_line_always: bool = True, 

655 ) -> None: 

656 ''' 

657 :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`. 

658 :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 

659 :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` 

660 :param config_instances: The settings supported in this config file. None means all settings which have been defined when this object is created. 

661 :param ignore: These settings are *not* supported by this config file even if they are contained in :paramref:`~confattr.configfile.ConfigFile.config_instances`. 

662 :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>`. Abstract classes are expanded to all non-abstract subclasses. 

663 :param ignore_commands: A sequence of commands (as subclasses of :class:`~confattr.configfile.ConfigFileCommand` or :class:`~confattr.configfile.ConfigFileArgparseCommand`) which are *not* allowed in this config file. May contain abstract classes. All commands which are contained in this sequence or which are a subclass of an item in this sequence are not allowed, regardless of whether they are passed to :paramref:`~confattr.configfile.ConfigFile.commands` or not. 

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

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

666 :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` 

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

668 ''' 

669 self.appname = appname 

670 self.authorname = authorname 

671 self.ui_notifier = UiNotifier(self, notification_level) 

672 state.has_any_config_file_been_instantiated = True 

673 if config_instances is None: 

674 # I am setting has_config_file_been_instantiated only if no config_instances have been passed 

675 # because if the user passes an explicit list of config_instances 

676 # then it's clear that Config instances created later on are ignored by this ConfigFile 

677 # so no TimingException should be raised if instantiating another Config. 

678 state.has_config_file_been_instantiated = True 

679 config_instances = Config.iter_instances() # this does not return a list or tuple and that is important for sorting 

680 self.config_instances = {i.key: i for i in self.iter_config_instances(config_instances, ignore)} 

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

682 self.formatter_class = formatter_class 

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

684 self.check_config_id = check_config_id 

685 self.show_line_always = show_line_always 

686 

687 if enable_config_ids is None: 

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

689 self.enable_config_ids = enable_config_ids 

690 

691 self.envprefix = '' 

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

693 envname = self.envprefix + 'CONFIG_PATH' 

694 self.env_variables.append(envname) 

695 if envname in os.environ: 

696 self.config_path = os.environ[envname] 

697 envname = self.envprefix + 'CONFIG_DIRECTORY' 

698 self.env_variables.append(envname) 

699 if envname in os.environ: 

700 self.config_directory = os.environ[envname] 

701 envname = self.envprefix + 'CONFIG_NAME' 

702 self.env_variables.append(envname) 

703 if envname in os.environ: 

704 self.config_name = os.environ[envname] 

705 

706 if commands is None: 

707 commands = ConfigFileCommand.get_command_types() 

708 else: 

709 original_commands = commands 

710 def iter_commands() -> 'Iterator[type[ConfigFileCommand]]': 

711 for cmd in original_commands: 

712 cmd = typing.cast('type[ConfigFileCommand]', cmd) 

713 if cmd._abstract: 

714 for c in ConfigFileCommand.get_command_types(): 

715 if issubclass(c, cmd): 

716 yield c 

717 else: 

718 yield cmd 

719 commands = iter_commands() 

720 self.command_dict = {} 

721 self.commands = [] 

722 for cmd_type in commands: 

723 if ignore_commands and any(issubclass(cmd_type, i_c) for i_c in ignore_commands): 

724 continue 

725 cmd = cmd_type(self) 

726 self.commands.append(cmd) 

727 for name in cmd.get_names(): 

728 self.command_dict[name] = cmd 

729 

730 def iter_config_instances(self, 

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

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

733 ) -> 'Iterator[Config[object]]': 

734 ''' 

735 :param config_instances: The settings to consider 

736 :param ignore: Skip these settings 

737 

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

739 Sort the resulting list if :paramref:`~confattr.configfile.ConfigFile.iter_config_instances.config_instances` is not a :class:`list` or a :class:`tuple`. 

740 Yield all :class:`~confattr.config.Config` instances which are not (directly or indirectly) contained in :paramref:`~confattr.configfile.ConfigFile.iter_config_instances.ignore`. 

741 ''' 

742 should_be_ignored: 'Callable[[Config[typing.Any]], bool]' 

743 if ignore is not None: 

744 tmp = set() 

745 for c in ignore: 

746 if isinstance(c, DictConfig): 

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

748 else: 

749 tmp.add(c) 

750 should_be_ignored = lambda c: c in tmp 

751 else: 

752 should_be_ignored = lambda c: False 

753 

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

755 config_instances = sorted(config_instances, key=lambda c: c.key_prefix if isinstance(c, DictConfig) else c.key) 

756 def expand_configs() -> 'Iterator[Config[typing.Any]]': 

757 for c in config_instances: 

758 if isinstance(c, DictConfig): 

759 yield from c.iter_configs() 

760 else: 

761 yield c 

762 for c in expand_configs(): 

763 if should_be_ignored(c): 

764 continue 

765 

766 yield c 

767 

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

769 ''' 

770 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. 

771 

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

773 

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

775 ''' 

776 self.ui_notifier.set_ui_callback(callback) 

777 

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

779 ''' 

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

781 

782 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. 

783 The first one installed is used. 

784 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. 

785 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``. 

786 

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

788 ''' 

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

790 try: 

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

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

793 except ImportError: 

794 try: 

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

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

797 except ImportError: 

798 AppDirs = appdirs.AppDirs 

799 

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

801 

802 return self._appdirs 

803 

804 # ------- load ------- 

805 

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

807 ''' 

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

809 

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

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

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

813 it's value is yielded and nothing else. 

814 ''' 

815 if self.config_directory: 

816 yield self.config_directory 

817 return 

818 

819 appdirs = self.get_app_dirs() 

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

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

822 

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

824 ''' 

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

826 

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

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

829 

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

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

832 

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

834 ''' 

835 if self.config_path: 

836 yield self.config_path 

837 return 

838 

839 for path in self.iter_user_site_config_paths(): 

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

841 

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

843 ''' 

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

845 

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

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

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

849 :return: False if an error has occurred 

850 ''' 

851 out = True 

852 for fn in self.iter_config_paths(): 

853 if os.path.isfile(fn): 

854 out &= self.load_file(fn) 

855 break 

856 

857 if env: 

858 out &= self.load_env() 

859 

860 return out 

861 

862 def load_env(self) -> bool: 

863 ''' 

864 Load settings from environment variables. 

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

866 

867 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>`. 

868 

869 :return: False if an error has occurred 

870 :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` 

871 ''' 

872 out = True 

873 old_file_name = self.context_file_name 

874 self.context_file_name = Message.ENVIRONMENT_VARIABLES 

875 

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

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

878 name = self.get_env_name(key) 

879 if name in self.env_variables: 

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

881 elif name in config_instances: 

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

883 else: 

884 config_instances[name] = instance 

885 

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

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

888 continue 

889 if name in self.env_variables: 

890 continue 

891 

892 if name in config_instances: 

893 instance = config_instances[name] 

894 try: 

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

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

897 except ValueError as e: 

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

899 out = False 

900 else: 

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

902 out = False 

903 

904 self.context_file_name = old_file_name 

905 return out 

906 

907 

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

909 ''' 

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

911 

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

913 ''' 

914 out = key 

915 out = out.upper() 

916 for c in ' .-': 

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

918 out = self.envprefix + out 

919 return out 

920 

921 def load_file(self, fn: str) -> bool: 

922 ''' 

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

924 

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

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

927 

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

929 :return: False if an error has occurred 

930 ''' 

931 self.config_id = None 

932 return self.load_without_resetting_config_id(fn) 

933 

934 def load_without_resetting_config_id(self, fn: str) -> bool: 

935 out = True 

936 old_file_name = self.context_file_name 

937 self.context_file_name = fn 

938 

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

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

941 self.context_line_number = lnno 

942 out &= self.parse_line(line=ln) 

943 self.context_line_number = None 

944 

945 self.context_file_name = old_file_name 

946 return out 

947 

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

949 ''' 

950 :param line: The line to be parsed 

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

952 

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

954 ''' 

955 ln = line.strip() 

956 if not ln: 

957 return True 

958 if self.is_comment(ln): 

959 return True 

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

961 return True 

962 

963 self.context_line = ln 

964 

965 try: 

966 ln_split = self.split_line(ln) 

967 except Exception as e: 

968 self.parse_error(str(e)) 

969 out = False 

970 else: 

971 out = self.parse_split_line(ln_split) 

972 

973 self.context_line = '' 

974 return out 

975 

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

977 cmd, line = self.split_one_symbol_command(line) 

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

979 if cmd: 

980 line_split.insert(0, cmd) 

981 return line_split 

982 

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

984 out = [] 

985 cmd, line = self.split_one_symbol_command(line) 

986 if cmd: 

987 out.append(cmd) 

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

989 lex.whitespace_split = True 

990 while True: 

991 try: 

992 t = lex.get_token() 

993 except: 

994 out.append(lex.token) 

995 return out 

996 if t is None: 

997 return out 

998 out.append(t) 

999 

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

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

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

1003 

1004 return None, line 

1005 

1006 

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

1008 ''' 

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

1010 

1011 :param line: The current line 

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

1013 ''' 

1014 for c in self.COMMENT_PREFIXES: 

1015 if line.startswith(c): 

1016 return True 

1017 return False 

1018 

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

1020 ''' 

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

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

1023 

1024 :param line: The current line 

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

1026 ''' 

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

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

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

1030 try: 

1031 self.check_config_id(config_id) 

1032 except ParseException as e: 

1033 self.parse_error(str(e)) 

1034 self.config_id = config_id 

1035 if self.config_id not in MultiConfig.config_ids: 

1036 MultiConfig.config_ids.append(self.config_id) 

1037 return True 

1038 return False 

1039 

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

1041 ''' 

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

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

1044 

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

1046 ''' 

1047 cmd = self.get_command(ln_split) 

1048 try: 

1049 cmd.run(ln_split) 

1050 except ParseException as e: 

1051 self.parse_error(str(e)) 

1052 return False 

1053 except MultipleParseExceptions as exceptions: 

1054 for exc in exceptions: 

1055 self.parse_error(str(exc)) 

1056 return False 

1057 

1058 return True 

1059 

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

1061 cmd_name = ln_split[0] 

1062 if cmd_name in self.command_dict: 

1063 cmd = self.command_dict[cmd_name] 

1064 elif DEFAULT_COMMAND in self.command_dict: 

1065 cmd = self.command_dict[DEFAULT_COMMAND] 

1066 else: 

1067 cmd = UnknownCommand(self) 

1068 return cmd 

1069 

1070 

1071 # ------- save ------- 

1072 

1073 def get_save_path(self) -> str: 

1074 ''' 

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

1076 ''' 

1077 paths = tuple(self.iter_config_paths()) 

1078 for fn in paths: 

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

1080 return fn 

1081 

1082 return paths[0] 

1083 

1084 def save(self, 

1085 if_not_existing: bool = False, 

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

1087 ) -> str: 

1088 ''' 

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

1090 Directories are created as necessary. 

1091 

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

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

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

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

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

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

1098 ''' 

1099 fn = self.get_save_path() 

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

1101 return fn 

1102 

1103 self.save_file(fn, **kw) 

1104 return fn 

1105 

1106 def save_file(self, 

1107 fn: str, 

1108 **kw: 'Unpack[SaveKwargs]' 

1109 ) -> None: 

1110 ''' 

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

1112 Directories are created as necessary, with `mode 0700 <https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation>`__ as specified by the `XDG Base Directory Specification standard <https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html>`__. 

1113 

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

1115 :raises FileNotFoundError: if the directory does not exist 

1116 

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

1118 ''' 

1119 # because os.path.dirname is not able to handle a file name without path 

1120 fn = os.path.abspath(fn) 

1121 

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

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

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

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

1126 

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

1128 self.save_to_open_file(f, **kw) 

1129 

1130 

1131 def save_to_open_file(self, 

1132 f: typing.TextIO, 

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

1134 ) -> None: 

1135 ''' 

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

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

1138 

1139 :param f: The file to write to 

1140 

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

1142 ''' 

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

1144 self.save_to_writer(writer, **kw) 

1145 

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

1147 ''' 

1148 Save the current values of all settings. 

1149 

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

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

1152 

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

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

1155 ''' 

1156 self.set_save_default_arguments(kw) 

1157 commands = list(self.commands) 

1158 if 'commands' in kw or 'ignore_commands' in kw: 

1159 command_types = tuple(kw['commands']) if 'commands' in kw else None 

1160 ignore_command_types = tuple(kw['ignore_commands']) if 'ignore_commands' in kw else None 

1161 for cmd in tuple(commands): 

1162 if (ignore_command_types and isinstance(cmd, ignore_command_types)) \ 

1163 or (command_types and not isinstance(cmd, command_types)): 

1164 commands.remove(cmd) 

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

1166 for cmd in commands: 

1167 cmd.should_write_heading = write_headings 

1168 cmd.save(writer, **kw) 

1169 

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

1171 ''' 

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

1173 ''' 

1174 kw.setdefault('config_instances', list(self.config_instances.values())) 

1175 kw.setdefault('ignore', None) 

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

1177 kw.setdefault('comments', True) 

1178 

1179 

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

1181 ''' 

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

1183 

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

1185 ''' 

1186 return readable_quote(val) 

1187 

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

1189 ''' 

1190 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`. 

1191 ''' 

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

1193 

1194 def get_help_config_id(self) -> str: 

1195 ''' 

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

1197 ''' 

1198 return f''' 

1199 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. 

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

1201 ''' 

1202 

1203 

1204 # ------- formatting and parsing of values ------- 

1205 

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

1207 ''' 

1208 :param instance: The config value to be saved 

1209 :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 

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

1211 

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

1213 ''' 

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

1215 

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

1217 return type.format_value(self, value) 

1218 

1219 

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

1221 ''' 

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

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

1224 :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 

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

1226 ''' 

1227 if not raw: 

1228 value = self.expand(value) 

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

1230 

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

1232 ''' 

1233 Parse a value to the given data type. 

1234 

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

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

1237 :param value: The value to be parsed 

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

1239 ''' 

1240 return t.parse_value(self, value) 

1241 

1242 

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

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

1245 

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

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

1248 n = arg.count('%') 

1249 if n % 2 == 1: 

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

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

1252 

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

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

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

1256 

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

1258 ''' 

1259 :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`` 

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

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

1262 

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

1264 

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

1266 

1267 ``!conversion`` is one of: 

1268 

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

1270 - ``!r``: :func:`repr` 

1271 - ``!s``: :class:`str` 

1272 - ``!a``: :func:`ascii` 

1273 

1274 ``: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>`__. 

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

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

1277 ''' 

1278 key = m.group(1) 

1279 if not key: 

1280 return '%' 

1281 

1282 if ':' in key: 

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

1284 else: 

1285 fmt = None 

1286 if '!' in key: 

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

1288 else: 

1289 stringifier = None 

1290 

1291 if key not in self.config_instances: 

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

1293 instance = self.config_instances[key] 

1294 

1295 if stringifier is None and fmt is None: 

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

1297 elif stringifier is None: 

1298 assert fmt is not None 

1299 try: 

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

1301 except Exception as e: 

1302 raise ParseException(e) 

1303 

1304 val: object 

1305 if stringifier == '': 

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

1307 else: 

1308 val = instance.get_value(config_id=None) 

1309 if stringifier == 'r': 

1310 val = repr(val) 

1311 elif stringifier == 's': 

1312 val = str(val) 

1313 elif stringifier == 'a': 

1314 val = ascii(val) 

1315 else: 

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

1317 

1318 if fmt is None: 

1319 assert isinstance(val, str) 

1320 return val 

1321 

1322 try: 

1323 return format(val, fmt) 

1324 except ValueError as e: 

1325 raise ParseException(e) 

1326 

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

1328 ''' 

1329 :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 

1330 :return: The expanded form of the environment variable 

1331 

1332 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: 

1333 

1334 - ``${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. 

1335 - ``${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. 

1336 - ``${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. 

1337 - ``${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. 

1338 

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

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

1341 ''' 

1342 env = m.group(1) 

1343 for op in '-=?+': 

1344 if ':' + op in env: 

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

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

1347 elif op in env: 

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

1349 isset = env in os.environ 

1350 else: 

1351 continue 

1352 

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

1354 if op == '-': 

1355 if isset: 

1356 return val 

1357 else: 

1358 return arg 

1359 elif op == '=': 

1360 if isset: 

1361 return val 

1362 else: 

1363 os.environ[env] = arg 

1364 return arg 

1365 elif op == '?': 

1366 if isset: 

1367 return val 

1368 else: 

1369 if not arg: 

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

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

1372 raise ParseException(arg) 

1373 elif op == '+': 

1374 if isset: 

1375 return arg 

1376 else: 

1377 return '' 

1378 else: 

1379 assert False 

1380 

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

1382 

1383 

1384 # ------- help ------- 

1385 

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

1387 import platform 

1388 formatter = self.create_formatter() 

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

1390 for path in self.iter_config_paths(): 

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

1392 

1393 writer.write_line('') 

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

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

1396 writer.write_line('- XDG_CONFIG_HOME') 

1397 writer.write_line('- XDG_CONFIG_DIRS') 

1398 for env in self.env_variables: 

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

1400 

1401 writer.write_line('') 

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

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

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

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

1406 

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

1408 

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

1410 for cmd in self.commands: 

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

1412 writer.write_heading(SectionLevel.SECTION, names) 

1413 writer.write_lines(cmd.get_help()) 

1414 

1415 def create_formatter(self) -> HelpFormatterWrapper: 

1416 return HelpFormatterWrapper(self.formatter_class) 

1417 

1418 def get_help(self) -> str: 

1419 ''' 

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

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

1422 

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

1424 ''' 

1425 doc = io.StringIO() 

1426 self.write_help(HelpWriter(doc)) 

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

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

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

1430 # Therefore I am stripping the trailing \n. 

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

1432 

1433 

1434 # ------- auto complete ------- 

1435 

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

1437 ''' 

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

1439 

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

1441 :param cursor_pos: The position of the cursor 

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

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

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

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

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

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

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

1449 *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. 

1450 ''' 

1451 original_ln = line 

1452 stripped_line = line.lstrip() 

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

1454 cursor_pos -= len(indentation) 

1455 line = stripped_line 

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

1457 out = self.get_completions_enter_group(line, cursor_pos) 

1458 else: 

1459 out = self.get_completions_command(line, cursor_pos) 

1460 

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

1462 return out 

1463 

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

1465 ''' 

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

1467 

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

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

1470 ''' 

1471 start = line 

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

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

1474 return '', groups, '' 

1475 

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

1477 ''' 

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

1479 

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

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

1482 ''' 

1483 if not line: 

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

1485 

1486 ln_split = self.split_line_ignore_errors(line) 

1487 assert ln_split 

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

1489 

1490 if a.in_between: 

1491 start_of_line = line[:cursor_pos] 

1492 end_of_line = line[cursor_pos:] 

1493 else: 

1494 start_of_line = line[:a.i0] 

1495 end_of_line = line[a.i1:] 

1496 

1497 if a.argument_pos == 0: 

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

1499 else: 

1500 cmd = self.get_command(ln_split) 

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

1502 

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

1504 ''' 

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

1506 ''' 

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

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

1509 out = ArgPos() 

1510 out.in_between = True 

1511 

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

1513 out.argument_pos = 0 

1514 out.i0 = 0 

1515 out.i1 = 0 

1516 

1517 n_ln = len(line) 

1518 i_ln = 0 

1519 n_arg = len(ln_split) 

1520 out.argument_pos = 0 

1521 i_in_arg = 0 

1522 assert out.argument_pos < n_ln 

1523 while True: 

1524 if out.in_between: 

1525 assert i_in_arg == 0 

1526 if i_ln >= n_ln: 

1527 assert out.argument_pos >= n_arg - 1 

1528 out.i0 = i_ln 

1529 return out 

1530 elif line[i_ln].isspace(): 

1531 i_ln += 1 

1532 else: 

1533 out.i0 = i_ln 

1534 if i_ln >= cursor_pos: 

1535 return out 

1536 if out.argument_pos >= n_arg: 

1537 assert line[i_ln] == '#' 

1538 out.i0 = len(line) 

1539 return out 

1540 out.in_between = False 

1541 else: 

1542 if i_ln >= n_ln: 

1543 assert out.argument_pos >= n_arg - 1 

1544 out.i1 = i_ln 

1545 return out 

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

1547 if line[i_ln].isspace(): 

1548 out.i1 = i_ln 

1549 if i_ln >= cursor_pos: 

1550 return out 

1551 out.in_between = True 

1552 i_ln += 1 

1553 out.argument_pos += 1 

1554 i_in_arg = 0 

1555 elif line[i_ln] in CHARS_REMOVED_BY_SHLEX: 

1556 i_ln += 1 

1557 else: 

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

1559 assert line[i_ln] == '#' 

1560 assert out.argument_pos == n_arg - 1 

1561 out.i1 = i_ln 

1562 return out 

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

1564 i_ln += 1 

1565 i_in_arg += 1 

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

1567 out.in_between = True 

1568 out.argument_pos += 1 

1569 out.i0 = i_ln 

1570 i_in_arg = 0 

1571 else: 

1572 assert line[i_ln] in CHARS_REMOVED_BY_SHLEX 

1573 i_ln += 1 

1574 

1575 

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

1577 start = line[:cursor_pos] 

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

1579 return start_of_line, completions, end_of_line 

1580 

1581 

1582 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]': 

1583 r''' 

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

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

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

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

1588 :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)``. 

1589 :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). 

1590 ''' 

1591 if exclude is None: 

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

1593 exclude = '$none' 

1594 else: 

1595 exclude = r'^\.' 

1596 reo = re.compile(exclude) 

1597 

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

1599 if os.path.sep in start: 

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

1601 directory += os.path.sep 

1602 quoted_directory = self.quote_path(directory) 

1603 

1604 start_of_line += quoted_directory 

1605 directory = os.path.expanduser(directory) 

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

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

1608 directory = os.path.normpath(directory) 

1609 else: 

1610 directory = relative_to 

1611 

1612 try: 

1613 names = os.listdir(directory) 

1614 except (FileNotFoundError, NotADirectoryError): 

1615 return start_of_line, [], end_of_line 

1616 

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

1618 for name in names: 

1619 if reo.match(name): 

1620 continue 

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

1622 continue 

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

1624 continue 

1625 

1626 quoted_name = self.quote(name) 

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

1628 quoted_name += os.path.sep 

1629 

1630 out.append(quoted_name) 

1631 

1632 return start_of_line, out, end_of_line 

1633 

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

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

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

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

1638 if path_split[i]: 

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

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

1641 

1642 

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

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

1645 if applicable: 

1646 return applicable, start_of_line, completions, end_of_line 

1647 

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

1649 

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

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

1652 return False, start_of_line, [], end_of_line 

1653 

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

1655 start_of_line = start_of_line + start[:i] 

1656 start = start[i:] 

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

1658 return True, start_of_line, completions, end_of_line 

1659 

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

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

1662 if i < 0: 

1663 return False, start_of_line, [], end_of_line 

1664 i += 2 

1665 

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

1667 return False, start_of_line, [], end_of_line 

1668 

1669 start_of_line = start_of_line + start[:i] 

1670 start = start[i:] 

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

1672 return True, start_of_line, completions, end_of_line 

1673 

1674 

1675 # ------- error handling ------- 

1676 

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

1678 ''' 

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

1680 

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

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

1683 

1684 :param msg: The error message 

1685 ''' 

1686 self.ui_notifier.show_error(msg) 

1687 

1688 

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

1690 

1691class ConfigFileCommand(abc.ABC): 

1692 

1693 ''' 

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

1695 

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

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

1698 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`. 

1699 

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

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

1702 ''' 

1703 

1704 #: 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. 

1705 name: str 

1706 

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

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

1709 

1710 #: 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. 

1711 help: str 

1712 

1713 #: 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. 

1714 should_write_heading: bool = False 

1715 

1716 #: 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`. 

1717 config_file: ConfigFile 

1718 

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

1720 ui_notifier: UiNotifier 

1721 

1722 _abstract: bool 

1723 

1724 

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

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

1727 

1728 @classmethod 

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

1730 ''' 

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

1732 ''' 

1733 return tuple(cls._subclasses) 

1734 

1735 @classmethod 

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

1737 ''' 

1738 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. 

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

1740 ''' 

1741 if cmd_type in cls._subclasses: 

1742 cls._subclasses.remove(cmd_type) 

1743 for name in cmd_type.get_names(): 

1744 cls._used_names.remove(name) 

1745 

1746 @classmethod 

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

1748 ''' 

1749 :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 

1750 :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` 

1751 :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 

1752 ''' 

1753 cls._abstract = abstract 

1754 if replace: 

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

1756 

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

1758 parent = parent_commands[0] 

1759 if 'name' not in cls.__dict__: 

1760 cls.name = parent.get_name() 

1761 if 'aliases' not in cls.__dict__: 

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

1763 for parent in parent_commands[1:]: 

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

1765 

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

1767 for parent in parent_commands: 

1768 cls.delete_command_type(parent) 

1769 

1770 if not abstract: 

1771 cls._subclasses.append(cls) 

1772 for name in cls.get_names(): 

1773 if name in cls._used_names and not replace: 

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

1775 cls._used_names.add(name) 

1776 

1777 @classmethod 

1778 def get_name(cls) -> str: 

1779 ''' 

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

1781  

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

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

1784 ''' 

1785 if 'name' in cls.__dict__: 

1786 return cls.name 

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

1788 

1789 @classmethod 

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

1791 ''' 

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

1793  

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

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

1796 

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

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

1799 ''' 

1800 yield cls.get_name() 

1801 if 'aliases' in cls.__dict__: 

1802 for name in cls.aliases: 

1803 yield name 

1804 

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

1806 self.config_file = config_file 

1807 self.ui_notifier = config_file.ui_notifier 

1808 

1809 @abc.abstractmethod 

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

1811 ''' 

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

1813 

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

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

1816 ''' 

1817 raise NotImplementedError() 

1818 

1819 

1820 def create_formatter(self) -> HelpFormatterWrapper: 

1821 return self.config_file.create_formatter() 

1822 

1823 def get_help_attr_or_doc_str(self) -> str: 

1824 ''' 

1825 :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`. 

1826 ''' 

1827 if hasattr(self, 'help'): 

1828 doc = self.help 

1829 elif self.__doc__: 

1830 doc = self.__doc__ 

1831 else: 

1832 doc = '' 

1833 

1834 return inspect.cleandoc(doc) 

1835 

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

1837 ''' 

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

1839 ''' 

1840 formatter.add_text(self.get_help_attr_or_doc_str()) 

1841 

1842 def get_help(self) -> str: 

1843 ''' 

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

1845 

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

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

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

1849 

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

1851 ''' 

1852 formatter = self.create_formatter() 

1853 self.add_help_to(formatter) 

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

1855 

1856 def get_short_description(self) -> str: 

1857 ''' 

1858 :return: The first paragraph of the doc string/help attribute 

1859 ''' 

1860 out = self.get_help_attr_or_doc_str().split('\n\n') 

1861 if out[0].startswith('usage: '): 

1862 if len(out) > 1: 

1863 return out[1] 

1864 return "" 

1865 return out[0] 

1866 

1867 def save(self, 

1868 writer: FormattedWriter, 

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

1870 ) -> None: 

1871 ''' 

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

1873 

1874 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. 

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

1876 

1877 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>`. 

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

1879 

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

1881 

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

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

1884 

1885 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>`. 

1886 

1887 The default implementation does nothing. 

1888 ''' 

1889 pass 

1890 

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

1892 

1893 

1894 # ------- auto complete ------- 

1895 

1896 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]': 

1897 ''' 

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

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

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

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

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

1903 :param end_of_line: The third return value. 

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

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

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

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

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

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

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

1911 ''' 

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

1913 return start_of_line, completions, end_of_line 

1914 

1915 

1916class ArgumentParser(argparse.ArgumentParser): 

1917 

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

1919 ''' 

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

1921 ''' 

1922 raise ParseException(message) 

1923 

1924class ConfigFileArgparseCommand(ConfigFileCommand, abstract=True): 

1925 

1926 ''' 

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

1928 

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

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

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

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

1933 

1934 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`. 

1935 ''' 

1936 

1937 #: 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` 

1938 parser: ArgumentParser 

1939 

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

1941 super().__init__(config_file) 

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

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

1944 self.init_parser(self.parser) 

1945 

1946 @abc.abstractmethod 

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

1948 ''' 

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

1950 

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

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

1953 ''' 

1954 pass 

1955 

1956 @staticmethod 

1957 def add_enum_argument(parser: 'argparse.ArgumentParser|argparse._MutuallyExclusiveGroup', *name_or_flags: str, type: 'type[enum.Enum]') -> 'argparse.Action': 

1958 ''' 

1959 This method: 

1960 

1961 - generates a function to convert the user input to an element of the enum 

1962 - gives the function the name of the enum in lower case (argparse uses this in error messages) 

1963 - generates a help string containing the allowed values 

1964 

1965 and adds an argument to the given argparse parser with that. 

1966 ''' 

1967 def parse(name: str) -> enum.Enum: 

1968 for v in type: 

1969 if v.name.lower() == name: 

1970 return v 

1971 raise TypeError() 

1972 parse.__name__ = type.__name__.lower() 

1973 choices = ', '.join(v.name.lower() for v in type) 

1974 return parser.add_argument(*name_or_flags, type=parse, help="one of " + choices) 

1975 

1976 def get_help(self) -> str: 

1977 ''' 

1978 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`. 

1979 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. 

1980 ''' 

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

1982 

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

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

1985 if not cmd: 

1986 return # pragma: no cover 

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

1988 if cmd[0] in self._names: 

1989 cmd = cmd[1:] 

1990 args = self.parser.parse_args(cmd) 

1991 self.run_parsed(args) 

1992 

1993 @abc.abstractmethod 

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

1995 ''' 

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

1997 ''' 

1998 pass 

1999 

2000 # ------- auto complete ------- 

2001 

2002 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]': 

2003 if in_between: 

2004 start = '' 

2005 else: 

2006 start = cmd[argument_pos][:cursor_pos] 

2007 

2008 if self.after_positional_argument_marker(cmd, argument_pos): 

2009 pos = self.get_position(cmd, argument_pos) 

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

2011 

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

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

2014 if prevarg: 

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

2016 

2017 if self.is_option_start(start): 

2018 if '=' in start: 

2019 i = start.index('=') 

2020 option_name = start[:i] 

2021 i += 1 

2022 start_of_line += start[:i] 

2023 start = start[i:] 

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

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

2026 

2027 pos = self.get_position(cmd, argument_pos) 

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

2029 

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

2031 ''' 

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

2033 ''' 

2034 pos = 0 

2035 n = len(cmd) 

2036 options_allowed = True 

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

2038 for i in range(1, argument_pos): 

2039 if options_allowed and i < n: 

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

2041 options_allowed = False 

2042 continue 

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

2044 continue 

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

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

2047 continue 

2048 pos += 1 

2049 

2050 return pos 

2051 

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

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

2054 

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

2056 ''' 

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

2058 ''' 

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

2060 

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

2062 if argument_pos >= len(cmd): 

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

2064 

2065 arg = cmd[argument_pos] 

2066 if '=' in arg: 

2067 # argument of option is already given within arg 

2068 return None 

2069 if not self.is_option_start(arg): 

2070 return None 

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

2072 action = self.get_action_for_option(arg) 

2073 if action is None: 

2074 return None 

2075 if action.nargs != 0: 

2076 return arg 

2077 return None 

2078 

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

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

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

2082 if action is None: 

2083 continue 

2084 if action.nargs != 0: 

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

2086 return None 

2087 

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

2089 action = self.get_action_for_option(out) 

2090 if action is None: 

2091 return None 

2092 if action.nargs != 0: 

2093 return out 

2094 return None 

2095 

2096 

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

2098 completions = [] 

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

2100 for opt in a.option_strings: 

2101 if len(opt) <= 2: 

2102 # this is trivial to type but not self explanatory 

2103 # => not helpful for auto completion 

2104 continue 

2105 if opt.startswith(start): 

2106 completions.append(opt) 

2107 return start_of_line, completions, end_of_line 

2108 

2109 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]': 

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

2111 

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

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

2114 

2115 

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

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

2118 if option_name in a.option_strings: 

2119 return a 

2120 return None 

2121 

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

2123 actions = self.parser._get_positional_actions() 

2124 if argument_pos < len(actions): 

2125 return actions[argument_pos] 

2126 return None 

2127 

2128 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]': 

2129 if action is None: 

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

2131 elif not action.choices: 

2132 completions = [] 

2133 else: 

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

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

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

2137 return start_of_line, completions, end_of_line 

2138 

2139 

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

2141 

2142class Set(ConfigFileCommand): 

2143 

2144 r''' 

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

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

2147 

2148 Change the value of a setting. 

2149 

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

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

2152 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. 

2153 

2154 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. 

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

2156 

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

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

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

2160 ''' 

2161 

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

2163 KEY_VAL_SEP = '=' 

2164 

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

2166 

2167 raw = False 

2168 

2169 # ------- load ------- 

2170 

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

2172 ''' 

2173 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`. 

2174 

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

2176 ''' 

2177 if self.is_vim_style(cmd): 

2178 self.set_multiple(cmd) 

2179 else: 

2180 self.set_with_spaces(cmd) 

2181 

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

2183 ''' 

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

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

2186 - 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`. 

2187 

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

2189 ''' 

2190 try: 

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

2192 if cmd[1] in self.FLAGS_RAW: 

2193 i = 2 

2194 else: 

2195 i = 1 

2196 return self.KEY_VAL_SEP in cmd[i] 

2197 except IndexError: 

2198 raise ParseException('no settings given') 

2199 

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

2201 ''' 

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

2203 

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

2205 ''' 

2206 if cmd[1] in self.FLAGS_RAW: 

2207 cmd = cmd[2:] 

2208 self.raw = True 

2209 else: 

2210 cmd = cmd[1:] 

2211 self.raw = False 

2212 

2213 n = len(cmd) 

2214 if n == 2: 

2215 key, value = cmd 

2216 self.parse_key_and_set_value(key, value) 

2217 elif n == 3: 

2218 key, sep, value = cmd 

2219 if sep != self.KEY_VAL_SEP: 

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

2221 self.parse_key_and_set_value(key, value) 

2222 elif n == 1: 

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

2224 else: 

2225 assert n >= 4 

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

2227 

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

2229 ''' 

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

2231 

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

2233 ''' 

2234 self.raw = False 

2235 exceptions = [] 

2236 for arg in cmd[1:]: 

2237 if arg in self.FLAGS_RAW: 

2238 self.raw = True 

2239 continue 

2240 try: 

2241 if not self.KEY_VAL_SEP in arg: 

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

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

2244 self.parse_key_and_set_value(key, value) 

2245 except ParseException as e: 

2246 exceptions.append(e) 

2247 if exceptions: 

2248 raise MultipleParseExceptions(exceptions) 

2249 

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

2251 ''' 

2252 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>`. 

2253 

2254 :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` 

2255 ''' 

2256 if key not in self.config_file.config_instances: 

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

2258 

2259 instance = self.config_file.config_instances[key] 

2260 try: 

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

2262 except ValueError as e: 

2263 raise ParseException(str(e)) 

2264 

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

2266 ''' 

2267 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`. 

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

2269 ''' 

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

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

2272 

2273 

2274 # ------- save ------- 

2275 

2276 def iter_config_instances_to_be_saved(self, 

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

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

2279 ) -> 'Iterator[Config[object]]': 

2280 ''' 

2281 Iterate over all :class:`~confattr.config.Config` instances yielded from :meth:`ConfigFile.iter_config_instances() <confattr.configfile.ConfigFile.iter_config_instances>` and yield all instances where :meth:`Config.wants_to_be_exported() <confattr.config.Config.wants_to_be_exported>` returns true. 

2282 ''' 

2283 for config in self.config_file.iter_config_instances(config_instances, ignore): 

2284 if config.wants_to_be_exported(): 

2285 yield config 

2286 

2287 #: 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`. 

2288 last_name: 'str|None' 

2289 

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

2291 ''' 

2292 :param writer: The file to write to 

2293 :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>`. 

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

2295 

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

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

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

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

2300 ''' 

2301 no_multi = kw['no_multi'] 

2302 comments = kw['comments'] 

2303 

2304 config_instances = list(self.iter_config_instances_to_be_saved(config_instances=kw['config_instances'], ignore=kw['ignore'])) 

2305 normal_configs = [] 

2306 multi_configs = [] 

2307 if no_multi: 

2308 normal_configs = config_instances 

2309 else: 

2310 for instance in config_instances: 

2311 if isinstance(instance, MultiConfig): 

2312 multi_configs.append(instance) 

2313 else: 

2314 normal_configs.append(instance) 

2315 

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

2317 

2318 if normal_configs: 

2319 if multi_configs: 

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

2321 elif self.should_write_heading: 

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

2323 

2324 if comments: 

2325 type_help = self.get_help_for_data_types(normal_configs) 

2326 if type_help: 

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

2328 writer.write_lines(type_help) 

2329 

2330 for instance in normal_configs: 

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

2332 

2333 if multi_configs: 

2334 if normal_configs: 

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

2336 elif self.should_write_heading: 

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

2338 

2339 if comments: 

2340 type_help = self.get_help_for_data_types(multi_configs) 

2341 if type_help: 

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

2343 writer.write_lines(type_help) 

2344 

2345 for instance in multi_configs: 

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

2347 

2348 for config_id in MultiConfig.config_ids: 

2349 writer.write_line('') 

2350 self.config_file.write_config_id(writer, config_id) 

2351 for instance in multi_configs: 

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

2353 

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

2355 ''' 

2356 :param writer: The file to write to 

2357 :param instance: The config value to be saved 

2358 :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 

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

2360 

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

2362 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`. 

2363 ''' 

2364 if kw['comments']: 

2365 self.write_config_help(writer, instance) 

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

2367 value = self.config_file.quote(value) 

2368 if '%' in value or '${' in value: 

2369 raw = ' --raw' 

2370 else: 

2371 raw = '' 

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

2373 writer.write_command(ln) 

2374 

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

2376 ''' 

2377 :param writer: The output to write to 

2378 :param instance: The config value to be saved 

2379 

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

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

2382 

2383 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. 

2384 ''' 

2385 if group_dict_configs and instance.parent is not None: 

2386 name = instance.parent.key_changer(instance.parent.key_prefix) 

2387 else: 

2388 name = instance.key 

2389 if name == self.last_name: 

2390 return 

2391 

2392 formatter = HelpFormatterWrapper(self.config_file.formatter_class) 

2393 writer.write_heading(SectionLevel.SUB_SECTION, name) 

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

2395 #if instance.unit: 

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

2397 if isinstance(instance.help, dict): 

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

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

2400 val = inspect.cleandoc(val) 

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

2402 elif isinstance(instance.help, str): 

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

2404 

2405 self.last_name = name 

2406 

2407 

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

2409 ''' 

2410 :param config_instances: All config values to be saved 

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

2412 

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

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

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

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

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

2418 ''' 

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

2420 for instance in config_instances: 

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

2422 name = t.get_type_name() 

2423 if name in help_text: 

2424 continue 

2425 

2426 h = t.get_help(self.config_file) 

2427 if not h: 

2428 continue 

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

2430 

2431 return help_text 

2432 

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

2434 help_map = self.get_data_type_name_to_help_map(config_instances) 

2435 if not help_map: 

2436 return 

2437 

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

2439 formatter.add_start_section(name) 

2440 formatter.add_text(help_map[name]) 

2441 formatter.add_end_section() 

2442 

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

2444 formatter = self.create_formatter() 

2445 self.add_help_for_data_types(formatter, config_instances) 

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

2447 

2448 # ------- help ------- 

2449 

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

2451 super().add_help_to(formatter) 

2452 

2453 config_instances = list(self.iter_config_instances_to_be_saved(config_instances=self.config_file.config_instances.values())) 

2454 self.last_name = None 

2455 

2456 formatter.add_start_section('data types') 

2457 self.add_help_for_data_types(formatter, config_instances) 

2458 formatter.add_end_section() 

2459 

2460 if self.config_file.enable_config_ids: 

2461 normal_configs = [] 

2462 multi_configs = [] 

2463 for instance in config_instances: 

2464 if isinstance(instance, MultiConfig): 

2465 multi_configs.append(instance) 

2466 else: 

2467 normal_configs.append(instance) 

2468 else: 

2469 normal_configs = config_instances 

2470 multi_configs = [] 

2471 

2472 if normal_configs: 

2473 if self.config_file.enable_config_ids: 

2474 formatter.add_start_section('application wide settings') 

2475 else: 

2476 formatter.add_start_section('settings') 

2477 for instance in normal_configs: 

2478 self.add_config_help(formatter, instance) 

2479 formatter.add_end_section() 

2480 

2481 if multi_configs: 

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

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

2484 for instance in multi_configs: 

2485 self.add_config_help(formatter, instance) 

2486 formatter.add_end_section() 

2487 

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

2489 formatter.add_start_section(instance.key) 

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

2491 if isinstance(instance.help, dict): 

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

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

2494 val = inspect.cleandoc(val) 

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

2496 elif isinstance(instance.help, str): 

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

2498 formatter.add_end_section() 

2499 

2500 # ------- auto complete ------- 

2501 

2502 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]': 

2503 if argument_pos >= len(cmd): 

2504 start = '' 

2505 else: 

2506 start = cmd[argument_pos][:cursor_pos] 

2507 

2508 if len(cmd) <= 1: 

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

2510 elif self.is_vim_style(cmd): 

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

2512 else: 

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

2514 

2515 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]': 

2516 if self.KEY_VAL_SEP in start: 

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

2518 start_of_line += key + self.KEY_VAL_SEP 

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

2520 else: 

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

2522 

2523 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]': 

2524 if argument_pos == 1: 

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

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

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

2528 else: 

2529 return start_of_line, [], end_of_line 

2530 

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

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

2533 return start_of_line, completions, end_of_line 

2534 

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

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

2537 if applicable: 

2538 return start_of_line, completions, end_of_line 

2539 

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

2541 if instance is None: 

2542 return start_of_line, [], end_of_line 

2543 

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

2545 

2546 

2547class Include(ConfigFileArgparseCommand): 

2548 

2549 ''' 

2550 Load another config file. 

2551 

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

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

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

2555 ''' 

2556 

2557 help_config_id = ''' 

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

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

2560 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. 

2561 

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

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

2564 ''' 

2565 

2566 home: 'Config[PathType]|str|None' = None 

2567 

2568 def get_home(self) -> str: 

2569 if not self.home: 

2570 home = "" 

2571 elif isinstance(self.home, str): 

2572 home = self.home 

2573 else: 

2574 home = self.home.expand() 

2575 if home: 

2576 return home 

2577 

2578 fn = self.config_file.context_file_name 

2579 if fn is None: 

2580 fn = self.config_file.get_save_path() 

2581 return os.path.dirname(fn) 

2582 

2583 

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

2585 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.') 

2586 if self.config_file.enable_config_ids: 

2587 assert parser.description is not None 

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

2589 group = parser.add_mutually_exclusive_group() 

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

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

2592 

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

2594 

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

2596 fn_imp = args.path 

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

2598 fn_imp = os.path.expanduser(fn_imp) 

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

2600 fn_imp = os.path.join(self.get_home(), fn_imp) 

2601 

2602 if fn_imp in self.nested_includes: 

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

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

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

2606 

2607 self.nested_includes.append(fn_imp) 

2608 

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

2610 self.config_file.load_without_resetting_config_id(fn_imp) 

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

2612 config_id = self.config_file.config_id 

2613 self.config_file.load_file(fn_imp) 

2614 self.config_file.config_id = config_id 

2615 else: 

2616 config_id = self.config_file.config_id 

2617 self.config_file.load_without_resetting_config_id(fn_imp) 

2618 self.config_file.config_id = config_id 

2619 

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

2621 del self.nested_includes[-1] 

2622 

2623 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]': 

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

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

2626 return self.config_file.get_completions_for_file_name(start, relative_to=self.get_home(), start_of_line=start_of_line, end_of_line=end_of_line) 

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

2628 

2629 

2630class Echo(ConfigFileArgparseCommand): 

2631 

2632 ''' 

2633 Display a message. 

2634 

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

2636 ''' 

2637 

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

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

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

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

2642 

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

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

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

2646 

2647 

2648 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]': 

2649 if argument_pos >= len(cmd): 

2650 start = '' 

2651 else: 

2652 start = cmd[argument_pos][:cursor_pos] 

2653 

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

2655 return start_of_line, completions, end_of_line 

2656 

2657class Help(ConfigFileArgparseCommand): 

2658 

2659 ''' 

2660 Display help. 

2661 ''' 

2662 

2663 max_width = 80 

2664 max_width_name = 18 

2665 min_width_sep = 2 

2666 tab_size = 4 

2667 

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

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

2670 

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

2672 if args.cmd: 

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

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

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

2676 out = cmd.get_help() 

2677 else: 

2678 out = "The following commands are defined:\n" 

2679 table = [] 

2680 for cmd in self.config_file.commands: 

2681 name = "- %s" % "/".join(cmd.get_names()) 

2682 descr = cmd.get_short_description() 

2683 row = (name, descr) 

2684 table.append(row) 

2685 out += self.format_table(table) 

2686 

2687 out += "\n" 

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

2689 

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

2691 

2692 def format_table(self, table: 'Sequence[tuple[str, str]]') -> str: 

2693 max_name_width = max(len(row[0]) for row in table) 

2694 col_width_name = min(max_name_width, self.max_width_name) 

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

2696 subsequent_indent = ' ' * (col_width_name + self.min_width_sep) 

2697 for name, descr in table: 

2698 if not descr: 

2699 out.append(name) 

2700 continue 

2701 if len(name) > col_width_name: 

2702 out.append(name) 

2703 initial_indent = subsequent_indent 

2704 else: 

2705 initial_indent = name.ljust(col_width_name + self.min_width_sep) 

2706 out.extend(textwrap.wrap(descr, self.max_width, 

2707 initial_indent = initial_indent, 

2708 subsequent_indent = subsequent_indent, 

2709 break_long_words = False, 

2710 tabsize = self.tab_size, 

2711 )) 

2712 return '\n'.join(out) 

2713 

2714 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]': 

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

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

2717 return start_of_line, completions, end_of_line 

2718 

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

2720 

2721 

2722class UnknownCommand(ConfigFileCommand, abstract=True): 

2723 

2724 name = DEFAULT_COMMAND 

2725 

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

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