Coverage for pyTooling/TerminalUI/__init__.py: 80%

575 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-03 23:02 +0000

1# ==================================================================================================================== # 

2# _____ _ _ _____ _ _ _ _ ___ # 

3# _ __ _ |_ _|__ ___ | (_)_ __ __ _|_ _|__ _ __ _ __ ___ (_)_ __ __ _| | | | |_ _| # 

4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |/ _ \ '__| '_ ` _ \| | '_ \ / _` | | | | || | # 

5# | |_) | |_| || | (_) | (_) | | | | | | (_| |_| | __/ | | | | | | | | | | | (_| | | |_| || | # 

6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_|\___|_| |_| |_| |_|_|_| |_|\__,_|_|\___/|___| # 

7# |_| |___/ |___/ # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # 

15# Copyright 2007-2016 Patrick Lehmann - Dresden, Germany # 

16# # 

17# Licensed under the Apache License, Version 2.0 (the "License"); # 

18# you may not use this file except in compliance with the License. # 

19# You may obtain a copy of the License at # 

20# # 

21# http://www.apache.org/licenses/LICENSE-2.0 # 

22# # 

23# Unless required by applicable law or agreed to in writing, software # 

24# distributed under the License is distributed on an "AS IS" BASIS, # 

25# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

26# See the License for the specific language governing permissions and # 

27# limitations under the License. # 

28# # 

29# SPDX-License-Identifier: Apache-2.0 # 

30# ==================================================================================================================== # 

31# 

32""" 

33A set of helpers to implement a text user interface (TUI) in a terminal. 

34 

35:raises MissingDependencyError: If the 'terminal' extra isn't installed. 

36 

37.. seealso:: 

38 

39 :mod:`pyTooling.Attributes.ArgParse` 

40 |rarr| Declaring the commands and options the application accepts. 

41 :mod:`pyTooling.CLIAbstraction` 

42 |rarr| Calling other programs from such an application. 

43 :mod:`pyTooling.Warning` 

44 |rarr| Collecting warnings that the application then writes. 

45""" 

46from __future__ import annotations 

47 

48from datetime import datetime 

49from enum import Enum, unique 

50from io import TextIOWrapper 

51from sys import stdin, stdout, stderr 

52from textwrap import dedent 

53from types import ModuleType 

54from typing import NoReturn, Any, Optional as Nullable, Callable, ClassVar 

55from pyTooling.Exceptions import MissingDependencyError 

56from pyTooling.Versioning import PythonVersion 

57 

58try: 

59 from colorama import Fore as Foreground 

60except ImportError as ex: # pragma: no cover 

61 raise MissingDependencyError(dependency="colorama", extra="terminal") from ex 

62 

63from pyTooling.Decorators import export, readonly 

64from pyTooling.MetaClasses import ExtendedType, mixin 

65from pyTooling.Exceptions import PlatformNotSupportedError, ExceptionBase 

66from pyTooling.Common import lastItem, getFullyQualifiedName 

67from pyTooling.Platform import Platform 

68 

69 

70@export 

71class TerminalBaseApplication(metaclass=ExtendedType, slots=True, singleton=True): 

72 """ 

73 The class offers a basic terminal application base-class. 

74 

75 It offers basic colored output via `colorama <https://GitHub.com/tartley/colorama>`__ as well as retrieving the 

76 terminal's width. 

77 """ 

78 

79 NOT_IMPLEMENTED_EXCEPTION_EXIT_CODE: ClassVar[int] = 240 #: Return code, if unimplemented methods or code sections were called. 

80 UNHANDLED_EXCEPTION_EXIT_CODE: ClassVar[int] = 241 #: Return code, if an unhandled exception reached the topmost exception handler. 

81 #: Return code (242), if an optional dependency is missing. The value lives on the exception, which stays 

82 #: importable when this module is not - see :meth:`PrintMissingDependencyError`. 

83 MISSING_DEPENDENCY_EXIT_CODE: ClassVar[int] = MissingDependencyError.EXIT_CODE 

84 FATAL_EXIT_CODE: ClassVar[int] = 255 #: Return code for fatal exits. 

85 ISSUE_TRACKER_URL: ClassVar[str] = None #: URL to the issue tracker for reporting bugs. 

86 INDENT: ClassVar[str] = " " #: Indentation. Default: ``" "`` (2 spaces) 

87 

88 try: 

89 from colorama import Fore as Foreground 

90 Foreground: ClassVar[dict[str, str]] = { 

91 "RED": Foreground.LIGHTRED_EX, 

92 "DARK_RED": Foreground.RED, 

93 "GREEN": Foreground.LIGHTGREEN_EX, 

94 "DARK_GREEN": Foreground.GREEN, 

95 "YELLOW": Foreground.LIGHTYELLOW_EX, 

96 "DARK_YELLOW": Foreground.YELLOW, 

97 "MAGENTA": Foreground.LIGHTMAGENTA_EX, 

98 "BLUE": Foreground.LIGHTBLUE_EX, 

99 "DARK_BLUE": Foreground.BLUE, 

100 "CYAN": Foreground.LIGHTCYAN_EX, 

101 "DARK_CYAN": Foreground.CYAN, 

102 "GRAY": Foreground.WHITE, 

103 "DARK_GRAY": Foreground.LIGHTBLACK_EX, 

104 "WHITE": Foreground.LIGHTWHITE_EX, 

105 "NOCOLOR": Foreground.RESET, 

106 

107 "HEADLINE": Foreground.LIGHTMAGENTA_EX, 

108 "ERROR": Foreground.LIGHTRED_EX, 

109 "WARNING": Foreground.LIGHTYELLOW_EX 

110 } #: Terminal colors 

111 except ImportError: # pragma: no cover 

112 Foreground: ClassVar[dict[str, str]] = { 

113 "RED": "", 

114 "DARK_RED": "", 

115 "GREEN": "", 

116 "DARK_GREEN": "", 

117 "YELLOW": "", 

118 "DARK_YELLOW": "", 

119 "MAGENTA": "", 

120 "BLUE": "", 

121 "DARK_BLUE": "", 

122 "CYAN": "", 

123 "DARK_CYAN": "", 

124 "GRAY": "", 

125 "DARK_GRAY": "", 

126 "WHITE": "", 

127 "NOCOLOR": "", 

128 

129 "HEADLINE": "", 

130 "ERROR": "", 

131 "WARNING": "" 

132 } #: Terminal colors 

133 

134 _stdin: TextIOWrapper #: STDIN 

135 _stdout: TextIOWrapper #: STDOUT 

136 _stderr: TextIOWrapper #: STDERR 

137 _width: int #: Terminal width in characters 

138 _height: int #: Terminal height in characters 

139 

140 def __init__(self) -> None: 

141 """ 

142 Initialize a terminal. 

143 

144 If the Python package `colorama <https://pypi.org/project/colorama/>`_ [#f_colorama]_ is available, then initialize 

145 it for colored outputs. 

146 

147 .. [#f_colorama] Colorama on Github: https://GitHub.com/tartley/colorama 

148 """ 

149 

150 self._stdin = stdin 

151 self._stdout = stdout 

152 self._stderr = stderr 

153 if stdout.isatty(): 153 ↛ 154line 153 didn't jump to line 154 because the condition on line 153 was never true

154 self.InitializeColors() 

155 else: 

156 self.UninitializeColors() 

157 self._width, self._height = self.GetTerminalSize() 

158 

159 def InitializeColors(self) -> bool: 

160 """ 

161 Initialize the terminal for color support by `colorama <https://GitHub.com/tartley/colorama>`__. 

162 

163 :returns: True, if 'colorama' package could be imported and initialized. 

164 """ 

165 try: 

166 from colorama import init 

167 

168 init() 

169 return True 

170 except ImportError: # pragma: no cover 

171 return False 

172 

173 def UninitializeColors(self) -> bool: 

174 """ 

175 Uninitialize the terminal for color support by `colorama <https://GitHub.com/tartley/colorama>`__. 

176 

177 :returns: True, if 'colorama' package could be imported and uninitialized. 

178 """ 

179 try: 

180 from colorama import deinit 

181 

182 deinit() 

183 return True 

184 except ImportError: # pragma: no cover 

185 return False 

186 

187 @readonly 

188 def Width(self) -> int: 

189 """ 

190 Read-only property to access the terminal's width. 

191 

192 :returns: The terminal window's width in characters. 

193 """ 

194 return self._width 

195 

196 @readonly 

197 def Height(self) -> int: 

198 """ 

199 Read-only property to access the terminal's height. 

200 

201 :returns: The terminal window's height in characters. 

202 """ 

203 return self._height 

204 

205 @staticmethod 

206 def GetTerminalSize() -> tuple[int, int]: 

207 """ 

208 Returns the terminal size as tuple (width, height) for Windows, macOS (Darwin), Linux, cygwin (Windows), MinGW32/64 (Windows). 

209 

210 :returns: A tuple containing width and height of the terminal's size in characters. 

211 :raises PlatformNotSupportedError: When a platform is not yet supported. 

212 """ 

213 platform = Platform() 

214 if platform.IsNativeWindows: 

215 size = TerminalBaseApplication.__GetTerminalSizeOnWindows() 

216 elif (platform.IsNativeLinux or platform.IsNativeFreeBSD or platform.IsNativeMacOS or platform.IsMinGW32OnWindows or platform.IsMinGW64OnWindows 

217 or platform.IsUCRT64OnWindows or platform.IsCygwin32OnWindows or platform.IsClang64OnWindows): 

218 size = TerminalBaseApplication.__GetTerminalSizeOnLinux() 

219 else: # pragma: no cover 

220 raise PlatformNotSupportedError(f"Platform '{platform}' not yet supported.") 

221 

222 if size is None: # pragma: no cover 

223 size = (80, 25) # default size 

224 

225 return size 

226 

227 @staticmethod 

228 def __GetTerminalSizeOnWindows() -> Nullable[tuple[int, int]]: 

229 """ 

230 Returns the current terminal window's size for Windows. 

231 

232 ``kernel32.dll:GetConsoleScreenBufferInfo()`` is used to retrieve the information. 

233 

234 :returns: A tuple containing width and height of the terminal's size in characters. 

235 """ 

236 try: 

237 from ctypes import windll, create_string_buffer 

238 from struct import unpack as struct_unpack 

239 

240 hStdError = windll.kernel32.GetStdHandle(-12) # stderr handle = -12 

241 stringBuffer = create_string_buffer(22) 

242 result = windll.kernel32.GetConsoleScreenBufferInfo(hStdError, stringBuffer) 

243 if result: 243 ↛ 244line 243 didn't jump to line 244 because the condition on line 243 was never true

244 bufx, bufy, curx, cury, wattr, left, top, right, bottom, maxx, maxy = struct_unpack("hhhhHhhhhhh", stringBuffer.raw) 

245 width = right - left + 1 

246 height = bottom - top + 1 

247 return width, height 

248 except ImportError: 

249 pass 

250 

251 return None 

252 # return Terminal.__GetTerminalSizeWithTPut() 

253 

254 # @staticmethod 

255 # def __GetTerminalSizeWithTPut() -> tuple[int, int]: 

256 # """ 

257 # Returns the current terminal window's size for Windows. 

258 # 

259 # ``tput`` is used to retrieve the information. 

260 # 

261 # :returns: A tuple containing width and height of the terminal's size in characters. 

262 # """ 

263 # from subprocess import check_output 

264 # 

265 # try: 

266 # width = int(check_output(("tput", "cols"))) 

267 # height = int(check_output(("tput", "lines"))) 

268 # return (width, height) 

269 # except: 

270 # pass 

271 

272 @staticmethod 

273 def __GetTerminalSizeOfFileDescriptor(fd: int) -> Nullable[tuple[int, int]]: 

274 """ 

275 Get window size of a file descriptor. 

276 

277 Call `ioctl` with ``TIOCGWINSZ`` (GetWindowsSize) for the given file descriptor. 

278 

279 :param fd: File descriptor to query. 

280 :returns: A 2-tuple of terminal width and height, or ``None`` if the size couldn't be determined. 

281 """ 

282 try: 

283 from array import array 

284 from fcntl import ioctl 

285 from termios import TIOCGWINSZ 

286 except ImportError: 

287 return None 

288 

289 # Allocate an array of 4x unsigned short (C struct) 

290 # H = unsigned short (16-bit) 

291 buffer = array('H', [0, 0, 0, 0]) # rows, columns, x-pixels, y-pixels 

292 try: 

293 ioctl(fd, TIOCGWINSZ, buffer, True) 

294 return buffer[1], buffer[0] 

295 except OSError: 

296 return None 

297 

298 @staticmethod 

299 def __GetTerminalSizeOnLinux() -> Nullable[tuple[int, int]]: 

300 """ 

301 Returns the current terminal window's size for Linux. 

302 

303 ``ioctl(TIOCGWINSZ)`` is used to retrieve the information. As a fallback, environment variables ``COLUMNS`` and 

304 ``LINES`` are checked. 

305 

306 :returns: A tuple containing width and height of the terminal's size in characters. 

307 """ 

308 # STDIN, STDOUT, STDERR 

309 for fd in range(3): 

310 if (size := TerminalBaseApplication.__GetTerminalSizeOfFileDescriptor(fd)) is not None: 310 ↛ 311line 310 didn't jump to line 311 because the condition on line 310 was never true

311 return size 

312 

313 # Fallback 

314 fd = None 

315 try: 

316 from os import open, close, ctermid, O_RDONLY 

317 

318 fd = open(ctermid(), O_RDONLY) 

319 if (size := TerminalBaseApplication.__GetTerminalSizeOfFileDescriptor(fd)) is not None: 

320 return size 

321 except (ImportError, OSError): 

322 # ImportError - If ctermid is not available (e.g. MSYS2) 

323 # OSError - If ctermid() or open() fails 

324 pass 

325 finally: 

326 if fd is not None: 326 ↛ 327line 326 didn't jump to line 327 because the condition on line 326 was never true

327 try: 

328 close(fd) 

329 except OSError: 

330 pass 

331 

332 # Fall-fallback 

333 from os import getenv 

334 

335 try: 

336 columns = int(getenv("COLUMNS")) 

337 lines = int(getenv("LINES")) 

338 return columns, lines 

339 except TypeError: 

340 pass 

341 

342 return None 

343 

344 def WriteToStdOut(self, message: str) -> int: 

345 """ 

346 Low-level method for writing to ``STDOUT``. 

347 

348 :param message: Message to write to ``STDOUT``. 

349 :returns: Number of written characters. 

350 """ 

351 return self._stdout.write(message) 

352 

353 def WriteLineToStdOut(self, message: str, end: str = "\n") -> int: 

354 """ 

355 Low-level method for writing to ``STDOUT``. 

356 

357 :param message: Message to write to ``STDOUT``. 

358 :param end: Optional, use newline character. Default: ``\\n``. 

359 :returns: Number of written characters. 

360 """ 

361 return self._stdout.write(message + end) 

362 

363 def WriteToStdErr(self, message: str) -> int: 

364 """ 

365 Low-level method for writing to ``STDERR``. 

366 

367 :param message: Message to write to ``STDERR``. 

368 :returns: Number of written characters. 

369 """ 

370 return self._stderr.write(message) 

371 

372 def WriteLineToStdErr(self, message: str, end: str = "\n") -> int: 

373 """ 

374 Low-level method for writing to ``STDERR``. 

375 

376 :param message: Message to write to ``STDERR``. 

377 :param end: Optional, use newline character. Default: ``\\n``. 

378 :returns: Number of written characters. 

379 """ 

380 return self._stderr.write(message + end) 

381 

382 def FatalExit(self, returnCode: int = 0) -> NoReturn: 

383 """ 

384 Exit the terminal application by uninitializing color support and returning a fatal Exit code. 

385 

386 :param returnCode: Optional, return code for application exit. 

387 """ 

388 self.Exit(self.FATAL_EXIT_CODE if returnCode == 0 else returnCode) 

389 

390 def Exit(self, returnCode: int = 0) -> NoReturn: 

391 """ 

392 Exit the terminal application by uninitializing color support and returning an Exit code. 

393 

394 :param returnCode: Optional, return code for application exit. 

395 """ 

396 self.UninitializeColors() 

397 exit(returnCode) 

398 

399 def PrintException(self, ex: Exception) -> NoReturn: 

400 """ 

401 Prints an exception of type :exc:`Exception` and its traceback. 

402 

403 If the exception as a nested action, the cause is printed as well. 

404 

405 If ``ISSUE_TRACKER_URL`` is configured, a URL to the issue tracker is added. 

406 

407 :param ex: The exception to print. 

408 """ 

409 from traceback import format_tb, walk_tb 

410 

411 frame, sourceLine = lastItem(walk_tb(ex.__traceback__)) 

412 filename = frame.f_code.co_filename 

413 funcName = frame.f_code.co_name 

414 

415 exceptionType = getFullyQualifiedName(ex) 

416 

417 message = f"{{RED}}[FATAL] An unknown or unhandled exception reached the topmost exception handler!{{NOCOLOR}}\n" 

418 message += f"{{indent}}{{YELLOW}}Exception type:{{NOCOLOR}} {{DARK_RED}}{exceptionType}{{NOCOLOR}}\n" 

419 message += f"{{indent}}{{YELLOW}}Exception message:{{NOCOLOR}} {{RED}}{ex!s}{{NOCOLOR}}\n" 

420 

421 if hasattr(ex, "__notes__") and len(ex.__notes__) > 0: 

422 note = next(iterator := iter(ex.__notes__)) 

423 message += f"{{indent}}{{YELLOW}}Notes:{{NOCOLOR}} {{DARK_CYAN}}{note}{{NOCOLOR}}\n" 

424 for note in iterator: 

425 message += f"{{indent}} {{DARK_CYAN}}{note}{{NOCOLOR}}\n" 

426 

427 message += f"{{indent}}{{YELLOW}}Caused in:{{NOCOLOR}} {funcName}(...) in file '{filename}' at line {sourceLine}\n" 

428 

429 if (ex2 := ex.__cause__) is not None: 

430 causeType = getFullyQualifiedName(ex2) 

431 

432 message += f"{{indent2}}{{DARK_YELLOW}}Caused by ex. type:{{NOCOLOR}} {{DARK_RED}}{causeType}{{NOCOLOR}}\n" 

433 message += f"{{indent2}}{{DARK_YELLOW}}Caused by message:{{NOCOLOR}} {ex2!s}{{NOCOLOR}}\n" 

434 

435 if hasattr(ex2, "__notes__") and len(ex2.__notes__) > 0: 435 ↛ 441line 435 didn't jump to line 441 because the condition on line 435 was always true

436 note = next(iterator := iter(ex2.__notes__)) 

437 message += f"{{indent2}}{{DARK_YELLOW}}Notes:{{NOCOLOR}} {{DARK_CYAN}}{note}{{NOCOLOR}}\n" 

438 for note in iterator: 

439 message += f"{{indent2}} {{DARK_CYAN}}{note}{{NOCOLOR}}\n" 

440 

441 message += f"{{indent}}{{RED}}{'-' * 120}{{NOCOLOR}}\n" 

442 for line in format_tb(ex.__traceback__): 

443 message += f"{line.replace('{', '{{').replace('}', '}}')}" 

444 message += f"{{indent}}{{RED}}{'-' * 120}{{NOCOLOR}}" 

445 

446 if self.ISSUE_TRACKER_URL is not None: 

447 message += f"\n{{indent}}{{DARK_CYAN}}Please report this bug at GitHub: {self.ISSUE_TRACKER_URL}{{NOCOLOR}}\n" 

448 message += f"{{indent}}{{RED}}{'-' * 120}{{NOCOLOR}}" 

449 

450 self.WriteLineToStdErr(message.format(indent=self.INDENT, indent2=self.INDENT*2, **self.Foreground)) 

451 self.Exit(self.UNHANDLED_EXCEPTION_EXIT_CODE) 

452 

453 def PrintMissingDependencyError(self, ex: MissingDependencyError) -> NoReturn: 

454 """ 

455 Print a missing optional dependency and the command lines installing it. 

456 

457 Unlike the other printers, this one does **not** report a bug: there is no traceback, and no invitation to 

458 open an issue, because nothing is wrong with the program - a package it can use is not installed. The message 

459 names the missing package and every installation option the exception carries 

460 (:attr:`~pyTooling.Exceptions.MissingDependencyError.InstallCommands`). 

461 

462 .. attention:: 

463 

464 :mod:`pyTooling.TerminalUI` raises this exception **itself** when *colorama* is missing, and that happens 

465 while the module is imported - long before an application object exists, so this method cannot report that 

466 case. An application that wants to survive it catches the exception around its own imports and prints the 

467 commands directly: 

468 

469 .. code-block:: python 

470 

471 from pyTooling.Exceptions import MissingDependencyError 

472 

473 try: 

474 from pyTooling.TerminalUI import TerminalApplication 

475 except MissingDependencyError as ex: 

476 print(f"{ex}\n" + "\n".join(f" {command}" for command in ex.InstallCommands)) 

477 raise SystemExit(MissingDependencyError.EXIT_CODE) from ex 

478 

479 :param ex: The exception to print. 

480 :returns: Never - the method exits the application with :attr:`MISSING_DEPENDENCY_EXIT_CODE`. 

481 

482 .. seealso:: 

483 

484 :meth:`PrintException` 

485 |rarr| Print an unhandled exception and its traceback. 

486 :meth:`PrintNotImplementedError` 

487 |rarr| Print a call to an unimplemented function or abstract method. 

488 """ 

489 message = f"{{RED}}[MISSING DEPENDENCY] An optional dependency is not installed!{{NOCOLOR}}\n" 

490 message += f"{{indent}}{{YELLOW}}Missing package:{{NOCOLOR}} {{DARK_RED}}{ex.Dependency}{{NOCOLOR}}\n" 

491 

492 commands = iter(ex.InstallCommands) 

493 message += f"{{indent}}{{YELLOW}}Install it with:{{NOCOLOR}} {{DARK_CYAN}}{next(commands)}{{NOCOLOR}}\n" 

494 for command in commands: 

495 message += f"{{indent}} {{DARK_CYAN}}{command}{{NOCOLOR}}\n" 

496 

497 if (cause := ex.__cause__) is not None: 

498 message += f"{{indent}}{{YELLOW}}Caused by:{{NOCOLOR}} {{RED}}{cause!s}{{NOCOLOR}}\n" 

499 

500 self.WriteLineToStdErr(message.format(indent=self.INDENT, indent2=self.INDENT * 2, **self.Foreground)) 

501 self.Exit(self.MISSING_DEPENDENCY_EXIT_CODE) 

502 

503 def PrintNotImplementedError(self, ex: NotImplementedError) -> NoReturn: 

504 """ 

505 Prints a not-implemented exception of type :exc:`NotImplementedError`. 

506 

507 If ``ISSUE_TRACKER_URL`` is configured, a URL to the issue tracker is added. 

508 

509 :param ex: The exception to print. 

510 """ 

511 from traceback import walk_tb 

512 

513 frame, sourceLine = lastItem(walk_tb(ex.__traceback__)) 

514 filename = frame.f_code.co_filename 

515 funcName = frame.f_code.co_name 

516 

517 message = f"{{RED}}[NOT IMPLEMENTED] An unimplemented function or abstract method was called!{{NOCOLOR}}\n" 

518 message += f"{{indent}}{{YELLOW}}Function or method:{{NOCOLOR}} {{DARK_RED}}{funcName}(...){{NOCOLOR}}\n" 

519 message += f"{{indent}}{{YELLOW}}Exception message:{{NOCOLOR}} {{RED}}{ex!s}{{NOCOLOR}}\n" 

520 

521 if hasattr(ex, "__notes__") and len(ex.__notes__) > 0: 521 ↛ 522line 521 didn't jump to line 522 because the condition on line 521 was never true

522 note = next(iterator := iter(ex.__notes__)) 

523 message += f"{{indent}}{{YELLOW}}Notes:{{NOCOLOR}} {{DARK_CYAN}}{note}{{NOCOLOR}}\n" 

524 for note in iterator: 

525 message += f"{{indent}} {{DARK_CYAN}}{note}{{NOCOLOR}}\n" 

526 

527 message += f"{{indent}}{{YELLOW}}Caused in:{{NOCOLOR}} {funcName}(...) in file '{filename}' at line {sourceLine}\n" 

528 

529 if self.ISSUE_TRACKER_URL is not None: 529 ↛ 533line 529 didn't jump to line 533 because the condition on line 529 was always true

530 message += f"\n{{indent}}{{DARK_CYAN}}Please report this bug at GitHub: {self.ISSUE_TRACKER_URL}{{NOCOLOR}}\n" 

531 message += f"{{indent}}{{RED}}{'-' * 120}{{NOCOLOR}}" 

532 

533 self.WriteLineToStdErr(message.format(indent=self.INDENT, indent2=self.INDENT * 2, **self.Foreground)) 

534 self.Exit(self.NOT_IMPLEMENTED_EXCEPTION_EXIT_CODE) 

535 

536 def PrintExceptionBase(self, ex: Exception) -> NoReturn: 

537 """ 

538 Prints an exception of type :exc:`~pyTooling.Exceptions.ExceptionBase` and its traceback. 

539 

540 If the exception as a nested action, the cause is printed as well. 

541 

542 If ``ISSUE_TRACKER_URL`` is configured, a URL to the issue tracker is added. 

543 

544 :param ex: The exception to print. 

545 """ 

546 from traceback import print_tb, walk_tb 

547 

548 frame, sourceLine = lastItem(walk_tb(ex.__traceback__)) 

549 filename = frame.f_code.co_filename 

550 funcName = frame.f_code.co_name 

551 

552 exceptionType = getFullyQualifiedName(ex) 

553 

554 self.WriteLineToStdErr(dedent(f"""\ 

555 {{RED}}[FATAL] A known but unhandled exception reached the topmost exception handler!{{NOCOLOR}} 

556 {{indent}}{{YELLOW}}Exception type:{{NOCOLOR}} {{DARK_RED}}{exceptionType}{{NOCOLOR}} 

557 {{indent}}{{YELLOW}}Exception message:{{NOCOLOR}} {{RED}}{ex!s}{{NOCOLOR}} 

558 {{indent}}{{YELLOW}}Caused in:{{NOCOLOR}} {funcName}(...) in file '{filename}' at line {sourceLine}\ 

559 """).format(indent=self.INDENT, **self.Foreground)) 

560 

561 if ex.__cause__ is not None: 

562 causeType = getFullyQualifiedName(ex.__cause__) 

563 

564 self.WriteLineToStdErr(dedent(f"""\ 

565 {{indent2}}{{DARK_YELLOW}}Caused by ex. type:{{NOCOLOR}} {{DARK_RED}}{causeType}{{NOCOLOR}} 

566 {{indent2}}{{DARK_YELLOW}}Caused by message:{{NOCOLOR}} {{RED}}{ex.__cause__!s}{{NOCOLOR}}\ 

567 """).format(indent2=self.INDENT * 2, **self.Foreground)) 

568 

569 self.WriteLineToStdErr(f"""{{indent}}{{RED}}{'-' * 80}{{NOCOLOR}}""".format(indent=self.INDENT, **self.Foreground)) 

570 print_tb(ex.__traceback__, file=self._stderr) 

571 self.WriteLineToStdErr(f"""{{indent}}{{RED}}{'-' * 80}{{NOCOLOR}}""".format(indent=self.INDENT, **self.Foreground)) 

572 

573 if self.ISSUE_TRACKER_URL is not None: 573 ↛ 579line 573 didn't jump to line 579 because the condition on line 573 was always true

574 self.WriteLineToStdErr(dedent(f"""\ 

575 {{indent}}{{DARK_CYAN}}Please report this bug at GitHub: {self.ISSUE_TRACKER_URL}{{NOCOLOR}} 

576 {{indent}}{{RED}}{'-' * 80}{{NOCOLOR}}\ 

577 """).format(indent=self.INDENT, **self.Foreground)) 

578 

579 self.Exit(self.UNHANDLED_EXCEPTION_EXIT_CODE) 

580 

581 

582@export 

583@unique 

584class Severity(Enum): 

585 """Logging message severity levels.""" 

586 

587 Exception = 120 #: Unhandled exception messages 

588 ExceptionCause = 115 #: Exception cause 

589 ExceptionNote = 110 #: Exception notes 

590 Fatal = 100 #: Fatal messages 

591 Error = 80 #: Error messages 

592 ErrorNote = 75 #: Error notes 

593 Quiet = 70 #: Always visible messages, even in quiet mode. 

594 

595 Critical = 60 #: Critical messages 

596 CriticalNote = 55 #: Critical notes 

597 Warning = 50 #: Warning messages 

598 WarningNote = 45 #: Warning notes 

599 Silent = 40 #: Severity level for silenced messages. 

600 

601 Info = 20 #: Informative messages 

602 Normal = 10 #: Normal messages 

603 DryRun = 8 #: Messages visible in a dry-run 

604 Verbose = 5 #: Verbose messages 

605 Debug = 2 #: Debug messages 

606 All = 0 #: All messages 

607 

608 def __hash__(self) -> int: 

609 """ 

610 Compute a hash of the severity level, so it can be used as a key in a dictionary. 

611 

612 :returns: Hash of the severity level's name. 

613 """ 

614 return hash(self.name) 

615 

616 def __eq__(self, other: Any) -> bool: 

617 """ 

618 Compare two Severity instances (severity level) for equality. 

619 

620 :param other: Operand to compare against. 

621 :returns: ``True``, if both severity levels are equal. 

622 :raises TypeError: If operand ``other`` is not of type :class:`Severity`. 

623 """ 

624 if isinstance(other, Severity): 

625 return self.value == other.value 

626 else: 

627 ex = TypeError("Second operand is not supported by == operator.") 

628 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

629 ex.add_note("Supported types for second operand: Severity") 

630 raise ex 

631 

632 def __ne__(self, other: Any) -> bool: 

633 """ 

634 Compare two Severity instances (severity level) for inequality. 

635 

636 :param other: Operand to compare against. 

637 :returns: ``True``, if both severity levels are unequal. 

638 :raises TypeError: If operand ``other`` is not of type :class:`Severity`. 

639 """ 

640 if isinstance(other, Severity): 

641 return self.value != other.value 

642 else: 

643 ex = TypeError("Second operand is not supported by != operator.") 

644 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

645 ex.add_note("Supported types for second operand: Severity") 

646 raise ex 

647 

648 def __lt__(self, other: Any) -> bool: 

649 """ 

650 Compare two Severity instances (severity level) for less-than. 

651 

652 :param other: Operand to compare against. 

653 :returns: ``True``, if severity levels is less than other severity level. 

654 :raises TypeError: If operand ``other`` is not of type :class:`Severity`. 

655 """ 

656 if isinstance(other, Severity): 

657 return self.value < other.value 

658 else: 

659 ex = TypeError("Second operand is not supported by < operator.") 

660 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

661 ex.add_note("Supported types for second operand: Severity") 

662 raise ex 

663 

664 def __le__(self, other: Any) -> bool: 

665 """ 

666 Compare two Severity instances (severity level) for less-than-or-equal. 

667 

668 :param other: Operand to compare against. 

669 :returns: ``True``, if severity levels is less than or equal other severity level. 

670 :raises TypeError: If operand ``other`` is not of type :class:`Severity`. 

671 """ 

672 if isinstance(other, Severity): 

673 return self.value <= other.value 

674 else: 

675 ex = TypeError("Second operand is not supported by <= operator.") 

676 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

677 ex.add_note("Supported types for second operand: Severity") 

678 raise ex 

679 

680 def __gt__(self, other: Any) -> bool: 

681 """ 

682 Compare two Severity instances (severity level) for greater-than. 

683 

684 :param other: Operand to compare against. 

685 :returns: ``True``, if severity levels is greater than other severity level. 

686 :raises TypeError: If operand ``other`` is not of type :class:`Severity`. 

687 """ 

688 if isinstance(other, Severity): 

689 return self.value > other.value 

690 else: 

691 ex = TypeError("Second operand is not supported by > operator.") 

692 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

693 ex.add_note("Supported types for second operand: Severity") 

694 raise ex 

695 

696 def __ge__(self, other: Any) -> bool: 

697 """ 

698 Compare two Severity instances (severity level) for greater-than-or-equal. 

699 

700 :param other: Operand to compare against. 

701 :returns: ``True``, if severity levels is greater than or equal other severity level. 

702 :raises TypeError: If operand ``other`` is not of type :class:`Severity`. 

703 """ 

704 if isinstance(other, Severity): 

705 return self.value >= other.value 

706 else: 

707 ex = TypeError("Second operand is not supported by >= operator.") 

708 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

709 ex.add_note("Supported types for second operand: Severity") 

710 raise ex 

711 

712 

713@export 

714@unique 

715class Mode(Enum): 

716 """Routing modes deciding to which stream (``STDOUT``/``STDERR``) a message of a certain severity is written.""" 

717 

718 TextToStdOut_ErrorsToStdErr = 0 #: Warnings and higher severities to ``STDERR``, except :attr:`Severity.Quiet`. 

719 AllLinearToStdOut = 1 #: All messages to ``STDOUT``, so the message order is preserved in a log file. 

720 DataToStdOut_OtherToStdErr = 2 #: All messages to ``STDERR``, leaving ``STDOUT`` for the program's data. 

721 

722 

723@export 

724class Line(metaclass=ExtendedType, slots=True): 

725 """ 

726 Represents a single message line with a severity and indentation level. 

727 """ 

728 

729 _LOG_MESSAGE_FORMAT__: ClassVar[dict[Severity, str]] = { 

730 Severity.Exception: "EXCEPTION: {message}", 

731 Severity.ExceptionNote: " > {message}", 

732 Severity.Fatal: "FATAL: {message}", 

733 Severity.Error: "ERROR: {message}", 

734 Severity.ErrorNote: " > {message}", 

735 Severity.Quiet: "{message}", 

736 Severity.Critical: "CRITICAL: {message}", 

737 Severity.CriticalNote: " > {message}", 

738 Severity.Warning: "WARNING: {message}", 

739 Severity.WarningNote: " > {message}", 

740 Severity.Info: "INFO: {message}", 

741 Severity.Normal: "{message}", 

742 Severity.DryRun: "DRYRUN: {message}", 

743 Severity.Verbose: "VERBOSE: {message}", 

744 Severity.Debug: "DEBUG: {message}", 

745 } #: Message line formatting rules. 

746 

747 _timestamp: datetime #: Timestamp when the line was created. 

748 _message: str #: Text message (line content). 

749 _severity: Severity #: Message severity 

750 _indent: int #: Indentation 

751 _appendLinebreak: bool #: True, if a trailing linebreak should be added when printing this line object. 

752 

753 def __init__( 

754 self, 

755 message: str, 

756 severity: Severity = Severity.Normal, 

757 *, 

758 indent: int = 0, 

759 appendLinebreak: bool = True 

760 ) -> None: 

761 """ 

762 Initialize a line object representing the single-line message. 

763 

764 :param message: Message to display. 

765 :param severity: Optional, severity level of the message. 

766 :param indent: Optional, indentation level of the message. 

767 :param appendLinebreak: Optional, if ``True``, append a line break at the end of the message. 

768 """ 

769 self._timestamp = datetime.now() 

770 self._severity = severity 

771 self._message = message 

772 self._indent = indent 

773 self._appendLinebreak = appendLinebreak 

774 

775 @readonly 

776 def Message(self) -> str: 

777 """ 

778 Read-only property to access the line's raw message. 

779 

780 :returns: Raw message of the line. 

781 """ 

782 return self._message 

783 

784 @readonly 

785 def Severity(self) -> Severity: 

786 """ 

787 Read-only property to access the line's severity level. 

788 

789 :returns: Severity level of the message line. 

790 """ 

791 return self._severity 

792 

793 @readonly 

794 def Indent(self) -> int: 

795 """ 

796 Read-only property to access the line's indentation level. 

797 

798 :returns: Indentation level of the message line. 

799 """ 

800 return self._indent 

801 

802 def IndentBy(self, indent: int) -> int: 

803 """ 

804 Increase a line's indentation level. 

805 

806 :param indent: Optional, indentation level added to the current indentation level. 

807 :returns: The new indentation level. 

808 """ 

809 self._indent = (newIndent := self._indent + indent) 

810 return newIndent 

811 

812 @readonly 

813 def AppendLinebreak(self) -> bool: 

814 """ 

815 Read-only property to access if a linebreak is added after the line's message. 

816 

817 :returns: True, if a linebreak should be added. 

818 """ 

819 return self._appendLinebreak 

820 

821 def __str__(self) -> str: 

822 """ 

823 Returns a formatted version of a ``Line`` objects as a string. 

824 

825 The formatting is defined in :attr:`_LOG_MESSAGE_FORMAT__`. 

826 

827 :returns: Formatted version of a ``Line`` object. 

828 """ 

829 return self._LOG_MESSAGE_FORMAT__[self._severity].format(message=self._message) 

830 

831 

832@export 

833@mixin 

834class ILineTerminal: 

835 """A mixin class (interface) to provide class-local terminal writing methods.""" 

836 

837 _terminal: Nullable[TerminalApplication] #: The terminal application the messages are written to. 

838 

839 def __init__(self, terminal: Nullable[TerminalApplication] = None) -> None: 

840 """ 

841 Mixin initializer. 

842 

843 :param terminal: Optional, the terminal to write to. If ``None``, every writing method does nothing. 

844 """ 

845 self._terminal = terminal 

846 

847 # FIXME: Alter methods if a terminal is present or set dummy methods 

848 

849 @readonly 

850 def Terminal(self) -> Nullable[TerminalApplication]: 

851 """ 

852 Read-only property to access the local terminal instance (:attr:`_terminal`). 

853 

854 :returns: The terminal instance, or ``None`` if no terminal is attached. 

855 """ 

856 return self._terminal 

857 

858 def WriteLine(self, line: Line, condition: bool = True) -> bool: 

859 """ 

860 Write a line to the local terminal if ``condition`` is ``True``. 

861 

862 :param line: Line object to write. 

863 :param condition: Optional, write the line only if this condition is ``True``. Default: ``True``. 

864 :returns: True, if the line was actually written. 

865 """ 

866 if (self._terminal is not None) and condition: 

867 return self._terminal.WriteLine(line) 

868 return False 

869 

870 # def _TryWriteLine(self, *args: Any, condition: bool = True, **kwargs: Any): 

871 # if (self._terminal is not None) and condition: 

872 # return self._terminal.TryWrite(*args, **kwargs) 

873 # return False 

874 

875 def WriteFatal(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool: 

876 """ 

877 Write a fatal message to the local terminal if ``condition`` is ``True``. 

878 

879 :param args: Positional parameters forwarded to the terminal's writing method. 

880 :param condition: Optional, write the message only if this condition is ``True``. Default: ``True``. 

881 :param kwargs: Keyword parameters forwarded to the terminal's writing method. 

882 :returns: True, if the message was actually written. 

883 """ 

884 if (self._terminal is not None) and condition: 

885 return self._terminal.WriteFatal(*args, **kwargs) 

886 return False 

887 

888 def WriteError(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool: 

889 """ 

890 Write an error message to the local terminal if ``condition`` is ``True``. 

891 

892 :param args: Positional parameters forwarded to the terminal's writing method. 

893 :param condition: Optional, write the message only if this condition is ``True``. Default: ``True``. 

894 :param kwargs: Keyword parameters forwarded to the terminal's writing method. 

895 :returns: True, if the message was actually written. 

896 """ 

897 if (self._terminal is not None) and condition: 

898 return self._terminal.WriteError(*args, **kwargs) 

899 return False 

900 

901 def WriteCritical(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool: 

902 """ 

903 Write a critical warning message to the local terminal if ``condition`` is ``True``. 

904 

905 :param args: Positional parameters forwarded to the terminal's writing method. 

906 :param condition: Optional, write the message only if this condition is ``True``. Default: ``True``. 

907 :param kwargs: Keyword parameters forwarded to the terminal's writing method. 

908 :returns: True, if the message was actually written. 

909 """ 

910 if (self._terminal is not None) and condition: 

911 return self._terminal.WriteCritical(*args, **kwargs) 

912 return False 

913 

914 def WriteWarning(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool: 

915 """ 

916 Write a warning message to the local terminal if ``condition`` is ``True``. 

917 

918 :param args: Positional parameters forwarded to the terminal's writing method. 

919 :param condition: Optional, write the message only if this condition is ``True``. Default: ``True``. 

920 :param kwargs: Keyword parameters forwarded to the terminal's writing method. 

921 :returns: True, if the message was actually written. 

922 """ 

923 if (self._terminal is not None) and condition: 

924 return self._terminal.WriteWarning(*args, **kwargs) 

925 return False 

926 

927 def WriteInfo(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool: 

928 """ 

929 Write an info message to the local terminal if ``condition`` is ``True``. 

930 

931 :param args: Positional parameters forwarded to the terminal's writing method. 

932 :param condition: Optional, write the message only if this condition is ``True``. Default: ``True``. 

933 :param kwargs: Keyword parameters forwarded to the terminal's writing method. 

934 :returns: True, if the message was actually written. 

935 """ 

936 if (self._terminal is not None) and condition: 

937 return self._terminal.WriteInfo(*args, **kwargs) 

938 return False 

939 

940 def WriteQuiet(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool: 

941 """ 

942 Write an always visible message, even in quiet mode, to the local terminal if ``condition`` is ``True``. 

943 

944 :param args: Positional parameters forwarded to the terminal's writing method. 

945 :param condition: Optional, write the message only if this condition is ``True``. Default: ``True``. 

946 :param kwargs: Keyword parameters forwarded to the terminal's writing method. 

947 :returns: True, if the message was actually written. 

948 """ 

949 if (self._terminal is not None) and condition: 

950 return self._terminal.WriteQuiet(*args, **kwargs) 

951 return False 

952 

953 def WriteNormal(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool: 

954 """ 

955 Write a *normal* message to the local terminal if ``condition`` is ``True``. 

956 

957 :param args: Positional parameters forwarded to the terminal's writing method. 

958 :param condition: Optional, write the message only if this condition is ``True``. Default: ``True``. 

959 :param kwargs: Keyword parameters forwarded to the terminal's writing method. 

960 :returns: True, if the message was actually written. 

961 """ 

962 if (self._terminal is not None) and condition: 

963 return self._terminal.WriteNormal(*args, **kwargs) 

964 return False 

965 

966 def WriteVerbose(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool: 

967 """ 

968 Write a verbose message to the local terminal if ``condition`` is ``True``. 

969 

970 :param args: Positional parameters forwarded to the terminal's writing method. 

971 :param condition: Optional, write the message only if this condition is ``True``. Default: ``True``. 

972 :param kwargs: Keyword parameters forwarded to the terminal's writing method. 

973 :returns: True, if the message was actually written. 

974 """ 

975 if (self._terminal is not None) and condition: 

976 return self._terminal.WriteVerbose(*args, **kwargs) 

977 return False 

978 

979 def WriteDebug(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool: 

980 """ 

981 Write a debug message to the local terminal if ``condition`` is ``True``. 

982 

983 :param args: Positional parameters forwarded to the terminal's writing method. 

984 :param condition: Optional, write the message only if this condition is ``True``. Default: ``True``. 

985 :param kwargs: Keyword parameters forwarded to the terminal's writing method. 

986 :returns: True, if the message was actually written. 

987 """ 

988 if (self._terminal is not None) and condition: 

989 return self._terminal.WriteDebug(*args, **kwargs) 

990 return False 

991 

992 def WriteDryRun(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool: 

993 """ 

994 Write a dry-run message to the local terminal if ``condition`` is ``True``. 

995 

996 :param args: Positional parameters forwarded to the terminal's writing method. 

997 :param condition: Optional, write the message only if this condition is ``True``. Default: ``True``. 

998 :param kwargs: Keyword parameters forwarded to the terminal's writing method. 

999 :returns: True, if the message was actually written. 

1000 """ 

1001 if (self._terminal is not None) and condition: 

1002 return self._terminal.WriteDryRun(*args, **kwargs) 

1003 return False 

1004 

1005 

1006@export 

1007class TerminalApplication(TerminalBaseApplication): #, ILineTerminal): 

1008 """ 

1009 A base-class for implementation of terminal applications emitting line-by-line messages. 

1010 """ 

1011 _LOG_MESSAGE_FORMAT__: ClassVar[dict[Severity, str]] = { 

1012 Severity.Exception: "{RED}[EXCEPTION] {message}{NOCOLOR}", 

1013 Severity.ExceptionNote: "{DARK_RED} > {message}{NOCOLOR}", 

1014 Severity.Fatal: "{DARK_RED}[FATAL] {message}{NOCOLOR}", 

1015 Severity.Error: "{RED}[ERROR] {message}{NOCOLOR}", 

1016 Severity.ErrorNote: "{DARK_RED} > {message}{NOCOLOR}", 

1017 Severity.Quiet: "{WHITE}{message}{NOCOLOR}", 

1018 Severity.Critical: "{DARK_YELLOW}[CRITICAL] {message}{NOCOLOR}", 

1019 Severity.CriticalNote: "{DARK_YELLOW} > {message}{NOCOLOR}", 

1020 Severity.Warning: "{YELLOW}[WARNING] {message}{NOCOLOR}", 

1021 Severity.WarningNote: "{DARK_YELLOW} > {message}{NOCOLOR}", 

1022 Severity.Info: "{WHITE}{message}{NOCOLOR}", 

1023 Severity.Normal: "{WHITE}{message}{NOCOLOR}", 

1024 Severity.DryRun: "{DARK_CYAN}[DRY] {message}{NOCOLOR}", 

1025 Severity.Verbose: "{GRAY}{message}{NOCOLOR}", 

1026 Severity.Debug: "{DARK_GRAY}{message}{NOCOLOR}" 

1027 } #: Message formatting rules. 

1028 

1029 _LOG_LEVEL_ROUTING__: dict[Severity, tuple[Callable[[str, str], int]]] #: Message routing rules. 

1030 _verbose: bool #: ``True``, if verbose messages are written. 

1031 _debug: bool #: ``True``, if debug messages are written. 

1032 _silent: bool #: ``True``, if no messages are written at all. 

1033 _quiet: bool #: ``True``, if only errors and quiet messages are written. 

1034 _writeLevel: Severity #: Minimal severity a message needs to be written. 

1035 _writeToStdOut: bool #: ``True``, if messages are written to ``STDOUT`` instead of ``STDERR``. 

1036 

1037 _lines: list[Line] #: Every message written so far, in the order it was written. 

1038 _baseIndent: int #: Indentation level added to every message's own indentation. 

1039 

1040 _errorCount: int #: Number of errors written so far. 

1041 _criticalWarningCount: int #: Number of critical warnings written so far. 

1042 _warningCount: int #: Number of warnings written so far. 

1043 

1044 HeadLine: ClassVar[str] #: Headline of the application, printed by :meth:`_PrintHeadline`. 

1045 

1046 def __init__(self, mode: Mode = Mode.AllLinearToStdOut) -> None: 

1047 """ 

1048 Initializer of a line-based terminal interface. 

1049 

1050 :param mode: Optional, defines what output (normal, error, data) to write where. Default: a linear flow all to 

1051 *STDOUT*. 

1052 """ 

1053 TerminalBaseApplication.__init__(self) 

1054 # ILineTerminal.__init__(self, self) 

1055 

1056 self._LOG_LEVEL_ROUTING__ = {} 

1057 self.__InitializeLogLevelRouting(mode) 

1058 

1059 self._verbose = False 

1060 self._debug = False 

1061 self._silent = False 

1062 self._quiet = False 

1063 self._writeLevel = Severity.Normal 

1064 self._writeToStdOut = True 

1065 

1066 self._lines = [] 

1067 self._baseIndent = 0 

1068 

1069 self._errorCount = 0 

1070 self._criticalWarningCount = 0 

1071 self._warningCount = 0 

1072 

1073 def __InitializeLogLevelRouting(self, mode: Mode = Mode.AllLinearToStdOut) -> None: 

1074 """ 

1075 Expand a routing mode into a routing table containing one writing method per severity level. 

1076 

1077 :param mode: Optional, routing mode to expand. 

1078 :raises ExceptionBase: If the routing mode is not supported. |br| 

1079 The note lists the modes that are supported. 

1080 """ 

1081 if mode is Mode.TextToStdOut_ErrorsToStdErr: 

1082 for severity in Severity: 

1083 if severity >= Severity.Silent and severity != Severity.Quiet: 

1084 self._LOG_LEVEL_ROUTING__[severity] = (self.WriteLineToStdErr,) 

1085 else: 

1086 self._LOG_LEVEL_ROUTING__[severity] = (self.WriteLineToStdOut,) 

1087 elif mode is Mode.AllLinearToStdOut: 

1088 for severity in Severity: 

1089 self._LOG_LEVEL_ROUTING__[severity] = (self.WriteLineToStdOut, ) 

1090 elif mode is Mode.DataToStdOut_OtherToStdErr: 

1091 for severity in Severity: 

1092 self._LOG_LEVEL_ROUTING__[severity] = (self.WriteLineToStdErr, ) 

1093 else: # pragma: no cover 

1094 ex = ExceptionBase(f"Unsupported mode '{mode}'.") 

1095 ex.add_note(f"Unsupported modes '{', '.join(m.name for m in Mode)}'.") 

1096 raise ex 

1097 

1098 def _PrintHeadline(self, width: int = 80) -> None: 

1099 """ 

1100 Helper method to print the program headline. 

1101 

1102 :param width: Optional, number of characters for horizontal lines. 

1103 

1104 .. admonition:: Generated output 

1105 

1106 .. code-block:: 

1107 

1108 ========================= 

1109 centered headline 

1110 ========================= 

1111 """ 

1112 if width == 0: 1112 ↛ 1113line 1112 didn't jump to line 1113 because the condition on line 1112 was never true

1113 width = self._width 

1114 

1115 self.WriteNormal(f"{{HEADLINE}}{'=' * width}".format(**TerminalApplication.Foreground)) 

1116 self.WriteNormal(f"{{HEADLINE}}{{headline: ^{width}s}}".format(headline=self.HeadLine, **TerminalApplication.Foreground)) 

1117 self.WriteNormal(f"{{HEADLINE}}{'=' * width}".format(**TerminalApplication.Foreground)) 

1118 

1119 def _PrintVersion( 

1120 self, 

1121 dunderModule: ModuleType, 

1122 packageName: Nullable[str] = None, 

1123 versionCheckTimeout: int = 1 

1124 ) -> None: 

1125 """ 

1126 Helper method to print the version information. 

1127 

1128 :param dunderModule: The Python module containing the dunder variables for author(s), email, copyright, 

1129 version, ... 

1130 :param packageName: Optional, name of the package on PyPI. If given, the latest released version is 

1131 queried and reported as an available update. Default: ``None``. 

1132 :param versionCheckTimeout: Optional, timeout in seconds for the PyPI request. Default: ``1``. 

1133 

1134 .. admonition:: Example usage 

1135 

1136 .. code-block:: Python 

1137 

1138 def _PrintVersion(self): 

1139 import myPackage.MyModule as DunderModule 

1140 

1141 super()._PrintVersion( 

1142 DunderModule, 

1143 "MyModule" 

1144 ) 

1145 """ 

1146 copyrights = getattr(dunderModule, "__copyright__", "{RED}Copyright not set!".format(RED=Foreground.RED)).split("\n", 1) 

1147 self.WriteNormal(f"Copyright: {copyrights[0]}") 

1148 for copyright in copyrights[1:]: 1148 ↛ 1149line 1148 didn't jump to line 1149 because the loop on line 1148 never started

1149 self.WriteNormal(f" {copyright}") 

1150 

1151 license = getattr(dunderModule, "__license__", "{RED}License not set!".format(RED=Foreground.RED)) 

1152 self.WriteNormal(f"License: {license}") 

1153 

1154 authors = getattr(dunderModule, "__author__", "{RED}Unknown author!".format(RED=Foreground.RED)).split(", ") 

1155 self.WriteNormal(f"Authors: {authors[0]}") 

1156 for author in authors[1:]: 1156 ↛ 1157line 1156 didn't jump to line 1157 because the loop on line 1156 never started

1157 self.WriteNormal(f" {author}") 

1158 

1159 if (email := getattr(dunderModule, "__email__", None)) is not None: 1159 ↛ 1162line 1159 didn't jump to line 1162 because the condition on line 1159 was always true

1160 self.WriteNormal(f"Email: {email}") 

1161 

1162 if (version := getattr(dunderModule, "__version__", None)) is None: 1162 ↛ 1163line 1162 didn't jump to line 1163 because the condition on line 1162 was never true

1163 self.WriteNormal("Version: {RED}Version not set!".format(RED=Foreground.RED)) 

1164 else: 

1165 currentVersion = PythonVersion.Parse(version) 

1166 if packageName is None: 1166 ↛ 1167line 1166 didn't jump to line 1167 because the condition on line 1166 was never true

1167 update = "" 

1168 elif (pypiVersion := self._GetLatestVersion(packageName, versionCheckTimeout)) is not None: 1168 ↛ 1172line 1168 didn't jump to line 1172 because the condition on line 1168 was always true

1169 latestVersion = PythonVersion.Parse(pypiVersion) 

1170 update = f" (Update available: v{latestVersion})" if currentVersion < latestVersion else " (latest)" 

1171 else: 

1172 update = " (PyPI timeout)" 

1173 self.WriteNormal(f"Version: v{version}{update}") 

1174 

1175 if (projectURL := getattr(dunderModule, "__project_url__", None)) is not None: 1175 ↛ 1178line 1175 didn't jump to line 1178 because the condition on line 1175 was always true

1176 self.WriteNormal(f"Project: {projectURL}") 

1177 

1178 if (documentationURL := getattr(dunderModule, "__documentation_url__", None)) is not None: 1178 ↛ 1181line 1178 didn't jump to line 1181 because the condition on line 1178 was always true

1179 self.WriteNormal(f"Documentation: {documentationURL}") 

1180 

1181 if (issueTrackerURL := getattr(dunderModule, "__issue_tracker_url__", None)) is not None: 1181 ↛ exitline 1181 didn't return from function '_PrintVersion' because the condition on line 1181 was always true

1182 self.WriteNormal(f"Issue tracker: {issueTrackerURL}") 

1183 

1184 def _GetLatestVersion(self, packageName: str, timeout: int = 1) -> Nullable[str]: 

1185 """ 

1186 Query PyPI for the latest released version of a package. 

1187 

1188 Every error - an unreachable index, a timeout, an unknown package - is answered with ``None``, because a version 

1189 check must not fail the application it is printing the version of. 

1190 

1191 :param packageName: Optional, name of the package on PyPI. 

1192 :param timeout: Optional, timeout in seconds for the request. Default: ``1``. 

1193 :returns: The latest version as a string, or ``None``, if it couldn't be determined. 

1194 """ 

1195 from json import loads 

1196 from urllib.request import urlopen, Request 

1197 

1198 request = Request( 

1199 url=f"https://pypi.org/pypi/{packageName}/json", 

1200 headers={'User-Agent': f'{packageName}-Version-Check'} 

1201 ) 

1202 try: 

1203 with urlopen(request, timeout=timeout) as response: 

1204 data: dict[str, dict[str, str]] = loads(response.read().decode()) 

1205 return data["info"]["version"] 

1206 except Exception: 

1207 return None 

1208 

1209 def Configure( 

1210 self, 

1211 *, 

1212 verbose: bool = False, 

1213 debug: bool = False, 

1214 silent: bool = False, 

1215 quiet: bool = False, 

1216 writeToStdOut: bool = True 

1217 ) -> None: 

1218 """ 

1219 Configure the verbosity of the application, usually from the command line switches. 

1220 

1221 The resulting :attr:`LogLevel` is the minimum severity a message needs to be written: ``Severity.Debug`` in debug 

1222 mode, ``Severity.Verbose`` in verbose mode, ``Severity.Silent`` in silent mode, ``Severity.Quiet`` in quiet mode, 

1223 otherwise ``Severity.Normal``. Debug mode implies verbose mode. 

1224 

1225 :param verbose: Optional, write verbose messages. Default: ``False``. 

1226 :param debug: Optional, write debug messages, implying verbose messages. Default: ``False``. 

1227 :param silent: Optional, reduce the messages to warnings and higher severities. Default: ``False``. 

1228 :param quiet: Optional, reduce the messages to errors and always visible messages. Default: ``False``. 

1229 :param writeToStdOut: Optional, write to ``STDOUT``. Default: ``True``. 

1230 """ 

1231 self._verbose = True if debug else verbose 

1232 self._debug = debug 

1233 self._silent = silent 

1234 self._quiet = quiet 

1235 

1236 if quiet: 

1237 self._writeLevel = Severity.Quiet 

1238 elif silent: 1238 ↛ 1239line 1238 didn't jump to line 1239 because the condition on line 1238 was never true

1239 self._writeLevel = Severity.Silent 

1240 elif debug: 1240 ↛ 1241line 1240 didn't jump to line 1241 because the condition on line 1240 was never true

1241 self._writeLevel = Severity.Debug 

1242 elif verbose: 1242 ↛ 1243line 1242 didn't jump to line 1243 because the condition on line 1242 was never true

1243 self._writeLevel = Severity.Verbose 

1244 else: 

1245 self._writeLevel = Severity.Normal 

1246 

1247 self._writeToStdOut = writeToStdOut 

1248 

1249 @readonly 

1250 def Verbose(self) -> bool: 

1251 """ 

1252 Check if verbose messages are enabled. 

1253 

1254 :returns: ``True``, if verbose messages are written. 

1255 """ 

1256 return self._verbose 

1257 

1258 @readonly 

1259 def Debug(self) -> bool: 

1260 """ 

1261 Check if debug messages are enabled. 

1262 

1263 :returns: ``True``, if debug messages are written. 

1264 """ 

1265 return self._debug 

1266 

1267 @readonly 

1268 def Silent(self) -> bool: 

1269 """ 

1270 Check if silent mode is enabled. 

1271 

1272 :returns: ``True``, if silent mode is enabled. 

1273 """ 

1274 return self._silent 

1275 

1276 @readonly 

1277 def Quiet(self) -> bool: 

1278 """ 

1279 Check if quiet mode is enabled. 

1280 

1281 :returns: ``True``, if quiet mode is enabled. 

1282 """ 

1283 return self._quiet 

1284 

1285 @property 

1286 def LogLevel(self) -> Severity: 

1287 """ 

1288 Property to access the minimal severity level a message needs to be written (:attr:`_writeLevel`). 

1289 

1290 Assigning a level replaces what :meth:`Configure` computed from the verbosity switches. 

1291 

1292 :returns: The current minimal severity level. 

1293 """ 

1294 return self._writeLevel 

1295 

1296 @LogLevel.setter 

1297 def LogLevel(self, value: Severity) -> None: 

1298 self._writeLevel = value 

1299 

1300 @property 

1301 def BaseIndent(self) -> int: 

1302 """ 

1303 Property to access the base indentation level of written messages (:attr:`_baseIndent`). 

1304 

1305 The assigned level is added to every message's own indentation. 

1306 

1307 :returns: Base indentation level. 

1308 """ 

1309 return self._baseIndent 

1310 

1311 @BaseIndent.setter 

1312 def BaseIndent(self, value: int) -> None: 

1313 self._baseIndent = value 

1314 

1315 @readonly 

1316 def WarningCount(self) -> int: 

1317 """ 

1318 Read-only property to access the number of counted warnings. 

1319 

1320 :returns: Number of warnings. 

1321 """ 

1322 return self._warningCount 

1323 

1324 @readonly 

1325 def CriticalWarningCount(self) -> int: 

1326 """ 

1327 Read-only property to access the number of counted critical warnings. 

1328 

1329 :returns: Number of critical warnings. 

1330 """ 

1331 return self._criticalWarningCount 

1332 

1333 @readonly 

1334 def ErrorCount(self) -> int: 

1335 """ 

1336 Read-only property to access the number of counted errors. 

1337 

1338 :returns: Number of errors. 

1339 """ 

1340 return self._errorCount 

1341 

1342 @readonly 

1343 def Lines(self) -> list[Line]: 

1344 """ 

1345 Read-only property to access the list of printed lines (messages). 

1346 

1347 :returns: List of lines. 

1348 """ 

1349 return self._lines 

1350 

1351 def ExitOnPreviousErrors(self) -> None: 

1352 """ 

1353 Exit application if errors have been printed. 

1354 """ 

1355 if self._errorCount > 0: 

1356 self.WriteFatal("Too many errors in previous steps.") 

1357 

1358 def ExitOnPreviousCriticalWarnings( 

1359 self, 

1360 includeErrors: bool = True 

1361 ) -> None: 

1362 """ 

1363 Exit application if error or critical warnings have been printed. 

1364 

1365 :param includeErrors: Optional, if ``True``, count previous errors as well as critical warnings. 

1366 """ 

1367 if includeErrors and (self._errorCount > 0): 1367 ↛ 1368line 1367 didn't jump to line 1368 because the condition on line 1367 was never true

1368 if self._criticalWarningCount > 0: 

1369 self.WriteFatal("Too many errors and critical warnings in previous steps.") 

1370 else: 

1371 self.WriteFatal("Too many errors in previous steps.") 

1372 elif self._criticalWarningCount > 0: 

1373 self.WriteFatal("Too many critical warnings in previous steps.") 

1374 

1375 def ExitOnPreviousWarnings( 

1376 self, 

1377 includeCriticalWarnings: bool = True, 

1378 includeErrors: bool = True 

1379 ) -> None: 

1380 """ 

1381 Exit application if error or (critical) warnings have been printed. 

1382 

1383 :param includeCriticalWarnings: Optional, if ``True``, count previous critical warnings as well as warnings. 

1384 :param includeErrors: Optional, if ``True``, count previous errors as well. 

1385 """ 

1386 if includeErrors and (self._errorCount > 0): 1386 ↛ 1387line 1386 didn't jump to line 1387 because the condition on line 1386 was never true

1387 if includeCriticalWarnings and (self._criticalWarningCount > 0): 

1388 if self._warningCount > 0: 

1389 self.WriteFatal("Too many errors and (critical) warnings in previous steps.") 

1390 else: 

1391 self.WriteFatal("Too many errors and critical warnings in previous steps.") 

1392 elif self._warningCount > 0: 

1393 self.WriteFatal("Too many warnings in previous steps.") 

1394 else: 

1395 self.WriteFatal("Too many errors in previous steps.") 

1396 elif includeCriticalWarnings and (self._criticalWarningCount > 0): 1396 ↛ 1397line 1396 didn't jump to line 1397 because the condition on line 1396 was never true

1397 if self._warningCount > 0: 

1398 self.WriteFatal("Too many (critical) warnings in previous steps.") 

1399 else: 

1400 self.WriteFatal("Too many critical warnings in previous steps.") 

1401 elif self._warningCount > 0: 

1402 self.WriteFatal("Too many warnings in previous steps.") 

1403 

1404 def WriteLine(self, line: Line) -> bool: 

1405 """ 

1406 Print a formatted line to the underlying terminal/console offered by the operating system. 

1407 

1408 The message is indented by :attr:`INDENT` repeated :attr:`Line.Indent` times. The indentation is applied to the 

1409 message, not to the whole line, so the severity markers stay in one column. 

1410 

1411 :param line: Line object to indent, format and print. 

1412 :returns: True, if line was actually written. 

1413 """ 

1414 if line.Severity < self._writeLevel: 

1415 return False 

1416 

1417 self._lines.append(line) 

1418 for method in self._LOG_LEVEL_ROUTING__[line.Severity]: 

1419 indentedMessage = self.INDENT * line.Indent + line.Message 

1420 method(self._LOG_MESSAGE_FORMAT__[line.Severity].format(message=indentedMessage, **self.Foreground), end="\n" if line.AppendLinebreak else "") 

1421 

1422 return True 

1423 

1424 def TryWriteLine(self, line) -> bool: 

1425 """ 

1426 Check if a line object of a certain severity would be written. 

1427 

1428 :param line: Line object to check. 

1429 :returns: True, if line would be written. 

1430 """ 

1431 severity: Severity = line.Severity # '@readonly' hands out 'Any' until it is typed - see T75 

1432 return severity >= self._writeLevel 

1433 

1434 def WriteFatal( 

1435 self, 

1436 message: str, 

1437 *, 

1438 indent: int = 0, 

1439 appendLinebreak: bool = True, 

1440 exitCode: int = 0, 

1441 immediateExit: bool = True 

1442 ) -> bool: 

1443 """ 

1444 Write a fatal message and exit. 

1445 

1446 Depending on internal settings and rules, a message might be skipped. 

1447 

1448 :param message: Message to write. 

1449 :param indent: Optional, indentation level of the message. 

1450 :param appendLinebreak: Optional, append a linebreak after the message. Default: ``True`` 

1451 :param exitCode: Optional, exit application with this exit code. Default: ``0`` |br| 

1452 If ``0``, use :attr:`FATAL_EXIT_CODE` as exit code. 

1453 :param immediateExit: Optional, exit application immediately. Default: ``True`` 

1454 :returns: True, if message was actually written. 

1455 """ 

1456 ret = self.WriteLine(Line(message, Severity.Fatal, indent=self._baseIndent + indent, appendLinebreak=appendLinebreak)) 

1457 if immediateExit: 

1458 self.FatalExit(exitCode) 

1459 return ret 

1460 

1461 def WriteError( 

1462 self, 

1463 message: str, 

1464 *, 

1465 indent: int = 0, 

1466 appendLinebreak: bool = True 

1467 ) -> bool: 

1468 """ 

1469 Write an error message. 

1470 

1471 Depending on internal settings and rules, a message might be skipped. 

1472 

1473 :param message: Message to write. 

1474 :param indent: Optional, indentation level of the message. 

1475 :param appendLinebreak: Optional, append a linebreak after the message. Default: ``True`` 

1476 :returns: True, if message was actually written. 

1477 """ 

1478 self._errorCount += 1 

1479 return self.WriteLine(Line(message, Severity.Error, indent=self._baseIndent + indent, appendLinebreak=appendLinebreak)) 

1480 

1481 def WriteQuiet( 

1482 self, 

1483 message: str, 

1484 *, 

1485 indent: int = 0, 

1486 appendLinebreak: bool = True 

1487 ) -> bool: 

1488 """ 

1489 Write an always visible message. 

1490 

1491 This message is even visible in quiet mode. 

1492 

1493 Depending on internal settings and rules, a message might be skipped. 

1494 

1495 :param message: Message to write. 

1496 :param indent: Optional, indentation level of the message. 

1497 :param appendLinebreak: Optional, append a linebreak after the message. Default: ``True`` 

1498 :returns: True, if message was actually written. 

1499 """ 

1500 return self.WriteLine(Line(message, Severity.Quiet, indent=self._baseIndent + indent, appendLinebreak=appendLinebreak)) 

1501 

1502 def WriteCritical( 

1503 self, 

1504 message: str, 

1505 *, 

1506 indent: int = 0, 

1507 appendLinebreak: bool = True 

1508 ) -> bool: 

1509 """ 

1510 Write a critical message. 

1511 

1512 Depending on internal settings and rules, a message might be skipped. 

1513 

1514 :param message: Message to write. 

1515 :param indent: Optional, indentation level of the message. 

1516 :param appendLinebreak: Optional, append a linebreak after the message. Default: ``True`` 

1517 :returns: True, if message was actually written. 

1518 """ 

1519 self._criticalWarningCount += 1 

1520 return self.WriteLine(Line(message, Severity.Critical, indent=self._baseIndent + indent, appendLinebreak=appendLinebreak)) 

1521 

1522 def WriteErrorNote( 

1523 self, 

1524 message: str, 

1525 *, 

1526 indent: int = 0, 

1527 appendLinebreak: bool = True 

1528 ) -> bool: 

1529 """ 

1530 Write a note belonging to an error, which is where the advice for fixing it goes. 

1531 

1532 Depending on internal settings and rules, a note might be skipped. 

1533 

1534 :param message: Message to write. 

1535 :param indent: Optional, indentation level of the note. 

1536 :param appendLinebreak: Optional, append a linebreak after the note. Default: ``True`` 

1537 :returns: True, if note was actually written. 

1538 """ 

1539 return self.WriteLine( 

1540 Line(message, Severity.ErrorNote, indent=self._baseIndent + indent, appendLinebreak=appendLinebreak) 

1541 ) 

1542 

1543 def WriteCriticalNote( 

1544 self, 

1545 message: str, 

1546 *, 

1547 indent: int = 0, 

1548 appendLinebreak: bool = True 

1549 ) -> bool: 

1550 """ 

1551 Write a critical note. 

1552 

1553 Depending on internal settings and rules, a note might be skipped. 

1554 

1555 :param message: Message to write. 

1556 :param indent: Optional, indentation level of the note. 

1557 :param appendLinebreak: Optional, append a linebreak after the note. Default: ``True`` 

1558 :returns: True, if note was actually written. 

1559 """ 

1560 return self.WriteLine(Line(message, Severity.CriticalNote, indent=self._baseIndent + indent, appendLinebreak=appendLinebreak)) 

1561 

1562 def WriteWarning( 

1563 self, 

1564 message: str, 

1565 *, 

1566 indent: int = 0, 

1567 appendLinebreak: bool = True 

1568 ) -> bool: 

1569 """ 

1570 Write a warning message. 

1571 

1572 Depending on internal settings and rules, a message might be skipped. 

1573 

1574 :param message: Message to write. 

1575 :param indent: Optional, indentation level of the message. 

1576 :param appendLinebreak: Optional, append a linebreak after the message. Default: ``True`` 

1577 :returns: True, if message was actually written. 

1578 """ 

1579 self._warningCount += 1 

1580 return self.WriteLine(Line(message, Severity.Warning, indent=self._baseIndent + indent, appendLinebreak=appendLinebreak)) 

1581 

1582 def WriteWarningNote( 

1583 self, 

1584 message: str, 

1585 *, 

1586 indent: int = 0, 

1587 appendLinebreak: bool = True 

1588 ) -> bool: 

1589 """ 

1590 Write a warning note. 

1591 

1592 Depending on internal settings and rules, a note might be skipped. 

1593 

1594 :param message: Message to write. 

1595 :param indent: Optional, indentation level of the note. 

1596 :param appendLinebreak: Optional, append a linebreak after the note. Default: ``True`` 

1597 :returns: True, if note was actually written. 

1598 """ 

1599 return self.WriteLine(Line(message, Severity.WarningNote, indent=self._baseIndent + indent, appendLinebreak=appendLinebreak)) 

1600 

1601 def WriteInfo( 

1602 self, 

1603 message: str, 

1604 *, 

1605 indent: int = 0, 

1606 appendLinebreak: bool = True 

1607 ) -> bool: 

1608 """ 

1609 Write an info message. 

1610 

1611 Depending on internal settings and rules, a message might be skipped. 

1612 

1613 :param message: Message to write. 

1614 :param indent: Optional, indentation level of the message. 

1615 :param appendLinebreak: Optional, append a linebreak after the message. Default: ``True`` 

1616 :returns: True, if message was actually written. 

1617 """ 

1618 return self.WriteLine(Line(message, Severity.Info, indent=self._baseIndent + indent, appendLinebreak=appendLinebreak)) 

1619 

1620 def WriteNormal( 

1621 self, 

1622 message: str, 

1623 *, 

1624 indent: int = 0, 

1625 appendLinebreak: bool = True 

1626 ) -> bool: 

1627 """ 

1628 Write a normal message. 

1629 

1630 Depending on internal settings and rules, a message might be skipped. 

1631 

1632 :param message: Message to write. 

1633 :param indent: Optional, indentation level of the message. 

1634 :param appendLinebreak: Optional, append a linebreak after the message. Default: ``True`` 

1635 :returns: True, if message was actually written. 

1636 """ 

1637 return self.WriteLine(Line(message, Severity.Normal, indent=self._baseIndent + indent, appendLinebreak=appendLinebreak)) 

1638 

1639 def WriteVerbose( 

1640 self, 

1641 message: str, 

1642 *, 

1643 indent: int = 0, 

1644 appendLinebreak: bool = True 

1645 ) -> bool: 

1646 """ 

1647 Write a verbose message. 

1648 

1649 Depending on internal settings and rules, a message might be skipped. 

1650 

1651 :param message: Message to write. 

1652 :param indent: Optional, indentation level of the message. 

1653 :param appendLinebreak: Optional, append a linebreak after the message. Default: ``True`` 

1654 :returns: True, if message was actually written. 

1655 """ 

1656 return self.WriteLine(Line(message, Severity.Verbose, indent=self._baseIndent + indent, appendLinebreak=appendLinebreak)) 

1657 

1658 def WriteDebug( 

1659 self, 

1660 message: str, 

1661 *, 

1662 indent: int = 0, 

1663 appendLinebreak: bool = True 

1664 ) -> bool: 

1665 """ 

1666 Write a debug message. 

1667 

1668 Depending on internal settings and rules, a message might be skipped. 

1669 

1670 :param message: Message to write. 

1671 :param indent: Optional, indentation level of the message. 

1672 :param appendLinebreak: Optional, append a linebreak after the message. Default: ``True`` 

1673 :returns: True, if message was actually written. 

1674 """ 

1675 return self.WriteLine(Line(message, Severity.Debug, indent=self._baseIndent + indent, appendLinebreak=appendLinebreak)) 

1676 

1677 def WriteDryRun( 

1678 self, 

1679 message: str, 

1680 *, 

1681 indent: int = 0, 

1682 appendLinebreak: bool = True 

1683 ) -> bool: 

1684 """ 

1685 Write a dry-run message message. 

1686 

1687 Depending on internal settings and rules, a message might be skipped. 

1688 

1689 :param message: Message to write. 

1690 :param indent: Optional, indentation level of the message. 

1691 :param appendLinebreak: Optional, append a linebreak after the message. Default: ``True`` 

1692 :returns: True, if message was actually written. 

1693 """ 

1694 return self.WriteLine(Line(message, Severity.DryRun, indent=self._baseIndent + indent, appendLinebreak=appendLinebreak))