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

645 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-25 12:33 +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 

35Colors need the optional package colorama (extra ``terminal``); without it, the output is uncolored text. 

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 os import getenv 

52from sys import stdin, stdout, stderr 

53from textwrap import dedent 

54from types import ModuleType 

55from typing import NoReturn, Any, Optional as Nullable, Callable, ClassVar, Protocol 

56 

57from pyTooling.Decorators import export, readonly, InheritDocString 

58from pyTooling.MetaClasses import ExtendedType, mixin 

59from pyTooling.Exceptions import MissingDependencyError, PlatformNotSupportedError, ExceptionBase 

60from pyTooling.Common import lastItem, getFullyQualifiedName 

61from pyTooling.Platform import Platform 

62from pyTooling.Versioning import PythonVersion 

63 

64 

65@export 

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

67 """ 

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

69 

70 It offers basic colored output via :gh:`colorama <tartley/colorama>` as well as retrieving the 

71 terminal's width. 

72 """ 

73 

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

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

76 MISSING_DEPENDENCY_EXIT_CODE: ClassVar[int] = MissingDependencyError.EXIT_CODE #: Return code (242), if an optional dependency is missing; the value lives on the exception - see :meth:`PrintMissingDependencyError`. 

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

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

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

80 

81 try: 

82 from colorama import Fore as Foreground 

83 FOREGROUND: ClassVar[dict[str, str]] = { 

84 "RED": Foreground.LIGHTRED_EX, 

85 "DARK_RED": Foreground.RED, 

86 "GREEN": Foreground.LIGHTGREEN_EX, 

87 "DARK_GREEN": Foreground.GREEN, 

88 "YELLOW": Foreground.LIGHTYELLOW_EX, 

89 "DARK_YELLOW": Foreground.YELLOW, 

90 "MAGENTA": Foreground.LIGHTMAGENTA_EX, 

91 "BLUE": Foreground.LIGHTBLUE_EX, 

92 "DARK_BLUE": Foreground.BLUE, 

93 "CYAN": Foreground.LIGHTCYAN_EX, 

94 "DARK_CYAN": Foreground.CYAN, 

95 "GRAY": Foreground.WHITE, 

96 "DARK_GRAY": Foreground.LIGHTBLACK_EX, 

97 "WHITE": Foreground.LIGHTWHITE_EX, 

98 "NOCOLOR": Foreground.RESET, 

99 

100 "HEADLINE": Foreground.LIGHTMAGENTA_EX, 

101 "ERROR": Foreground.LIGHTRED_EX, 

102 "WARNING": Foreground.LIGHTYELLOW_EX 

103 } #: Terminal colors 

104 except ImportError: # pragma: no cover 

105 FOREGROUND: ClassVar[dict[str, str]] = { 

106 "RED": "", 

107 "DARK_RED": "", 

108 "GREEN": "", 

109 "DARK_GREEN": "", 

110 "YELLOW": "", 

111 "DARK_YELLOW": "", 

112 "MAGENTA": "", 

113 "BLUE": "", 

114 "DARK_BLUE": "", 

115 "CYAN": "", 

116 "DARK_CYAN": "", 

117 "GRAY": "", 

118 "DARK_GRAY": "", 

119 "WHITE": "", 

120 "NOCOLOR": "", 

121 

122 "HEADLINE": "", 

123 "ERROR": "", 

124 "WARNING": "" 

125 } #: Terminal colors 

126 

127 _stdin: TextIOWrapper #: STDIN 

128 _stdout: TextIOWrapper #: STDOUT 

129 _stderr: TextIOWrapper #: STDERR 

130 _width: int #: Terminal width in characters 

131 _height: int #: Terminal height in characters 

132 

133 def __init__(self) -> None: 

134 """ 

135 Initialize a terminal. 

136 

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

138 it for colored outputs. 

139 

140 .. [#f_colorama] Colorama on GitHub: :gh:`tartley/colorama` 

141 """ 

142 

143 self._stdin = stdin 

144 self._stdout = stdout 

145 self._stderr = stderr 

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

147 self.InitializeColors() 

148 else: 

149 self.UninitializeColors() 

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

151 

152 def InitializeColors(self) -> bool: 

153 """ 

154 Initialize the terminal for color support by :gh:`colorama <tartley/colorama>`. 

155 

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

157 """ 

158 try: 

159 from colorama import init 

160 

161 init() 

162 return True 

163 except ImportError: # pragma: no cover 

164 return False 

165 

166 def UninitializeColors(self) -> bool: 

167 """ 

168 Uninitialize the terminal for color support by :gh:`colorama <tartley/colorama>`. 

169 

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

171 """ 

172 try: 

173 from colorama import deinit 

174 

175 deinit() 

176 return True 

177 except ImportError: # pragma: no cover 

178 return False 

179 

180 @readonly 

181 def Width(self) -> int: 

182 """ 

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

184 

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

186 """ 

187 return self._width 

188 

189 @readonly 

190 def Height(self) -> int: 

191 """ 

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

193 

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

195 """ 

196 return self._height 

197 

198 @staticmethod 

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

200 """ 

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

202 

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

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

205 """ 

206 platform = Platform() 

207 if platform.IsNativeWindows: 

208 size = TerminalBaseApplication.__GetTerminalSizeOnWindows() 

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

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

211 size = TerminalBaseApplication.__GetTerminalSizeOnLinux() 

212 else: # pragma: no cover 

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

214 

215 if size is None: # pragma: no cover 

216 size = (80, 25) # default size 

217 

218 return size 

219 

220 @staticmethod 

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

222 """ 

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

224 

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

226 

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

228 """ 

229 try: 

230 from ctypes import windll, create_string_buffer 

231 from struct import unpack as struct_unpack 

232 

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

234 stringBuffer = create_string_buffer(22) 

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

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

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

238 width = right - left + 1 

239 height = bottom - top + 1 

240 return width, height 

241 except ImportError: 

242 pass 

243 

244 return None 

245 # return Terminal.__GetTerminalSizeWithTPut() 

246 

247 # @staticmethod 

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

249 # """ 

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

251 # 

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

253 # 

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

255 # """ 

256 # from subprocess import check_output 

257 # 

258 # try: 

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

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

261 # return (width, height) 

262 # except: 

263 # pass 

264 

265 @staticmethod 

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

267 """ 

268 Get window size of a file descriptor. 

269 

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

271 

272 :param fd: File descriptor to query. 

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

274 """ 

275 try: 

276 from array import array 

277 from fcntl import ioctl 

278 from termios import TIOCGWINSZ 

279 except ImportError: 

280 return None 

281 

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

283 # H = unsigned short (16-bit) 

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

285 try: 

286 ioctl(fd, TIOCGWINSZ, buffer, True) 

287 return buffer[1], buffer[0] 

288 except OSError: 

289 return None 

290 

291 @staticmethod 

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

293 """ 

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

295 

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

297 ``LINES`` are checked. 

298 

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

300 """ 

301 # STDIN, STDOUT, STDERR 

302 for fd in range(3): 

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

304 return size 

305 

306 # Fallback 

307 fd = None 

308 try: 

309 from os import open, close, ctermid, O_RDONLY 

310 

311 fd = open(ctermid(), O_RDONLY) 

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

313 return size 

314 except (ImportError, OSError): 

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

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

317 pass 

318 finally: 

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

320 try: 

321 close(fd) 

322 except OSError: 

323 pass 

324 

325 # Fall-fallback 

326 try: 

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

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

329 return columns, lines 

330 except TypeError: 

331 pass 

332 

333 return None 

334 

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

336 """ 

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

338 

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

340 :returns: Number of written characters. 

341 """ 

342 return self._stdout.write(message) 

343 

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

345 """ 

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

347 

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

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

350 :returns: Number of written characters. 

351 """ 

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

353 

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

355 """ 

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

357 

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

359 :returns: Number of written characters. 

360 """ 

361 return self._stderr.write(message) 

362 

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

364 """ 

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

366 

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

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

369 :returns: Number of written characters. 

370 """ 

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

372 

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

374 """ 

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

376 

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

378 """ 

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

380 

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

382 """ 

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

384 

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

386 """ 

387 self.UninitializeColors() 

388 exit(returnCode) 

389 

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

391 """ 

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

393 

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

395 

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

397 

398 :param ex: The exception to print. 

399 """ 

400 from traceback import format_tb, walk_tb 

401 

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

403 filename = frame.f_code.co_filename 

404 funcName = frame.f_code.co_name 

405 

406 exceptionType = getFullyQualifiedName(ex) 

407 

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

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

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

411 

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

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

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

415 for note in iterator: 

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

417 

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

419 

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

421 causeType = getFullyQualifiedName(ex2) 

422 

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

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

425 

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

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

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

429 for note in iterator: 

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

431 

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

433 for line in format_tb(ex.__traceback__): 

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

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

436 

437 if self.ISSUE_TRACKER_URL is not None: 

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

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

440 

441 self.WriteLineToStdErr(message.format(indent=self.INDENT, indent2=self.INDENT*2, **self.FOREGROUND)) 

442 self.Exit(self.UNHANDLED_EXCEPTION_EXIT_CODE) 

443 

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

445 """ 

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

447 

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

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

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

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

452 

453 .. attention:: 

454 

455 A dependency missing while a module is imported raises before an application object exists, so this method 

456 cannot report that case. An application that wants to survive it catches the exception around its own imports 

457 and prints the commands directly: 

458 

459 .. code-block:: python 

460 

461 from pyTooling.Exceptions import MissingDependencyError 

462 

463 try: 

464 from myPackage.Plotting import Plot 

465 except MissingDependencyError as ex: 

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

467 raise SystemExit(MissingDependencyError.EXIT_CODE) from ex 

468 

469 :param ex: The exception to print. 

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

471 

472 .. seealso:: 

473 

474 :meth:`PrintException` 

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

476 :meth:`PrintNotImplementedError` 

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

478 """ 

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

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

481 

482 commands = iter(ex.InstallCommands) 

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

484 for command in commands: 

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

486 

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

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

489 

490 self.WriteLineToStdErr(message.format(indent=self.INDENT, indent2=self.INDENT * 2, **self.FOREGROUND)) 

491 self.Exit(self.MISSING_DEPENDENCY_EXIT_CODE) 

492 

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

494 """ 

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

496 

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

498 

499 :param ex: The exception to print. 

500 """ 

501 from traceback import walk_tb 

502 

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

504 filename = frame.f_code.co_filename 

505 funcName = frame.f_code.co_name 

506 

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

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

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

510 

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

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

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

514 for note in iterator: 

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

516 

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

518 

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

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

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

522 

523 self.WriteLineToStdErr(message.format(indent=self.INDENT, indent2=self.INDENT * 2, **self.FOREGROUND)) 

524 self.Exit(self.NOT_IMPLEMENTED_EXCEPTION_EXIT_CODE) 

525 

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

527 """ 

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

529 

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

531 

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

533 

534 :param ex: The exception to print. 

535 """ 

536 from traceback import print_tb, walk_tb 

537 

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

539 filename = frame.f_code.co_filename 

540 funcName = frame.f_code.co_name 

541 

542 exceptionType = getFullyQualifiedName(ex) 

543 

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

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

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

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

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

549 """).format(indent=self.INDENT, **self.FOREGROUND)) 

550 

551 if ex.__cause__ is not None: 

552 causeType = getFullyQualifiedName(ex.__cause__) 

553 

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

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

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

557 """).format(indent2=self.INDENT * 2, **self.FOREGROUND)) 

558 

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

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

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

562 

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

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

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

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

567 """).format(indent=self.INDENT, **self.FOREGROUND)) 

568 

569 self.Exit(self.UNHANDLED_EXCEPTION_EXIT_CODE) 

570 

571 

572@export 

573@unique 

574class Severity(Enum): 

575 """Logging message severity levels.""" 

576 

577 Exception = 120 #: Unhandled exception messages 

578 ExceptionCause = 115 #: Exception cause 

579 ExceptionNote = 110 #: Exception notes 

580 Fatal = 100 #: Fatal messages 

581 Error = 80 #: Error messages 

582 ErrorNote = 75 #: Error notes 

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

584 

585 Critical = 60 #: Critical messages 

586 CriticalNote = 55 #: Critical notes 

587 Warning = 50 #: Warning messages 

588 WarningNote = 45 #: Warning notes 

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

590 

591 Info = 20 #: Informative messages 

592 Normal = 10 #: Normal messages 

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

594 Verbose = 5 #: Verbose messages 

595 Debug = 2 #: Debug messages 

596 All = 0 #: All messages 

597 

598 def __hash__(self) -> int: 

599 """ 

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

601 

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

603 """ 

604 return hash(self.name) 

605 

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

607 """ 

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

609 

610 :param other: Operand to compare against. 

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

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

613 """ 

614 if isinstance(other, Severity): 

615 return self.value == other.value 

616 else: 

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

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

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

620 raise ex 

621 

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

623 """ 

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

625 

626 :param other: Operand to compare against. 

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

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

629 """ 

630 if isinstance(other, Severity): 

631 return self.value != other.value 

632 else: 

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

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

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

636 raise ex 

637 

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

639 """ 

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

641 

642 :param other: Operand to compare against. 

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

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

645 """ 

646 if isinstance(other, Severity): 

647 return self.value < other.value 

648 else: 

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

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

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

652 raise ex 

653 

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

655 """ 

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

657 

658 :param other: Operand to compare against. 

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

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

661 """ 

662 if isinstance(other, Severity): 

663 return self.value <= other.value 

664 else: 

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

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

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

668 raise ex 

669 

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

671 """ 

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

673 

674 :param other: Operand to compare against. 

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

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

677 """ 

678 if isinstance(other, Severity): 

679 return self.value > other.value 

680 else: 

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

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

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

684 raise ex 

685 

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

687 """ 

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

689 

690 :param other: Operand to compare against. 

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

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

693 """ 

694 if isinstance(other, Severity): 

695 return self.value >= other.value 

696 else: 

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

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

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

700 raise ex 

701 

702 

703@export 

704@unique 

705class Mode(Enum): 

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

707 

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

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

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

711 

712 

713@export 

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

715 """ 

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

717 """ 

718 

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

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

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

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

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

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

725 Severity.Quiet: "{message}", 

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

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

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

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

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

731 Severity.Normal: "{message}", 

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

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

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

735 } #: Message line formatting rules. 

736 

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

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

739 _severity: Severity #: Message severity 

740 _indent: int #: Indentation 

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

742 

743 def __init__( 

744 self, 

745 message: str, 

746 severity: Severity = Severity.Normal, 

747 *, 

748 indent: int = 0, 

749 appendLinebreak: bool = True 

750 ) -> None: 

751 """ 

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

753 

754 :param message: Message to display. 

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

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

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

758 """ 

759 self._timestamp = datetime.now() 

760 self._severity = severity 

761 self._message = message 

762 self._indent = indent 

763 self._appendLinebreak = appendLinebreak 

764 

765 @readonly 

766 def Message(self) -> str: 

767 """ 

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

769 

770 :returns: Raw message of the line. 

771 """ 

772 return self._message 

773 

774 @readonly 

775 def Severity(self) -> Severity: 

776 """ 

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

778 

779 :returns: Severity level of the message line. 

780 """ 

781 return self._severity 

782 

783 @readonly 

784 def Indent(self) -> int: 

785 """ 

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

787 

788 :returns: Indentation level of the message line. 

789 """ 

790 return self._indent 

791 

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

793 """ 

794 Increase a line's indentation level. 

795 

796 :param indent: Indentation level added to the current indentation level. 

797 :returns: The new indentation level. 

798 """ 

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

800 return newIndent 

801 

802 @readonly 

803 def AppendLinebreak(self) -> bool: 

804 """ 

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

806 

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

808 """ 

809 return self._appendLinebreak 

810 

811 def __str__(self) -> str: 

812 """ 

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

814 

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

816 

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

818 """ 

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

820 

821 

822@export 

823@mixin 

824class ILineTerminal: 

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

826 

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

828 

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

830 """ 

831 Mixin initializer. 

832 

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

834 """ 

835 self._terminal = terminal 

836 

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

838 

839 @readonly 

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

841 """ 

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

843 

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

845 """ 

846 return self._terminal 

847 

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

849 """ 

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

851 

852 :param line: Line object to write. 

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

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

855 """ 

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

857 return self._terminal.WriteLine(line) 

858 return False 

859 

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

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

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

863 # return False 

864 

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

866 """ 

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

868 

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

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

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

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

873 """ 

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

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

876 return False 

877 

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

879 """ 

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

881 

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

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

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

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

886 """ 

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

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

889 return False 

890 

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

892 """ 

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

894 

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

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

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

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

899 """ 

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

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

902 return False 

903 

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

905 """ 

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

907 

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

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

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

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

912 """ 

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

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

915 return False 

916 

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

918 """ 

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

920 

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

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

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

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

925 """ 

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

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

928 return False 

929 

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

931 """ 

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

933 

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

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

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

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

938 """ 

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

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

941 return False 

942 

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

944 """ 

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

946 

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

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

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

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

951 """ 

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

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

954 return False 

955 

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

957 """ 

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

959 

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

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

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

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

964 """ 

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

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

967 return False 

968 

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

970 """ 

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

972 

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

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

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

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

977 """ 

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

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

980 return False 

981 

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

983 """ 

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

985 

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

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

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

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

990 """ 

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

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

993 return False 

994 

995 

996@export 

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

998 """ 

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

1000 """ 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1017 } #: Message formatting rules. 

1018 

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

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

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

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

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

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

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

1026 

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

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

1029 

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

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

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

1033 

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

1035 

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

1037 """ 

1038 Initializer of a line-based terminal interface. 

1039 

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

1041 *STDOUT*. 

1042 """ 

1043 TerminalBaseApplication.__init__(self) 

1044 # ILineTerminal.__init__(self, self) 

1045 

1046 self._LOG_LEVEL_ROUTING__ = {} 

1047 self.__InitializeLogLevelRouting(mode) 

1048 

1049 self._verbose = False 

1050 self._debug = False 

1051 self._silent = False 

1052 self._quiet = False 

1053 self._writeLevel = Severity.Normal 

1054 self._writeToStdOut = True 

1055 

1056 self._lines = [] 

1057 self._baseIndent = 0 

1058 

1059 self._errorCount = 0 

1060 self._criticalWarningCount = 0 

1061 self._warningCount = 0 

1062 

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

1064 """ 

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

1066 

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

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

1069 The note lists the modes that are supported. 

1070 """ 

1071 if mode is Mode.TextToStdOut_ErrorsToStdErr: 

1072 for severity in Severity: 

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

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

1075 else: 

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

1077 elif mode is Mode.AllLinearToStdOut: 

1078 for severity in Severity: 

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

1080 elif mode is Mode.DataToStdOut_OtherToStdErr: 

1081 for severity in Severity: 

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

1083 else: # pragma: no cover 

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

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

1086 raise ex 

1087 

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

1089 """ 

1090 Helper method to print the program headline. 

1091 

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

1093 

1094 .. admonition:: Generated output 

1095 

1096 .. code-block:: 

1097 

1098 ========================= 

1099 centered headline 

1100 ========================= 

1101 """ 

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

1103 width = self._width 

1104 

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

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

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

1108 

1109 def _PrintVersion( 

1110 self, 

1111 dunderModule: ModuleType, 

1112 packageName: Nullable[str] = None, 

1113 versionCheckTimeout: int = 1 

1114 ) -> None: 

1115 """ 

1116 Helper method to print the version information. 

1117 

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

1119 version, ... 

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

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

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

1123 

1124 .. admonition:: Example usage 

1125 

1126 .. code-block:: Python 

1127 

1128 def _PrintVersion(self): 

1129 import myPackage.MyModule as DunderModule 

1130 

1131 super()._PrintVersion( 

1132 DunderModule, 

1133 "MyModule" 

1134 ) 

1135 """ 

1136 red = self.FOREGROUND["DARK_RED"] 

1137 

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

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

1140 for copyright in copyrights[1:]: 

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

1142 

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

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

1145 

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

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

1148 for author in authors[1:]: 

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

1150 

1151 if (email := getattr(dunderModule, "__email__", None)) is not None: 

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

1153 

1154 if (version := getattr(dunderModule, "__version__", None)) is None: 

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

1156 else: 

1157 currentVersion = PythonVersion.Parse(version) 

1158 if packageName is None: 

1159 update = "" 

1160 elif (pypiVersion := self._GetLatestVersion(packageName, versionCheckTimeout)) is not None: 

1161 latestVersion = PythonVersion.Parse(pypiVersion) 

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

1163 else: 

1164 update = " (PyPI timeout)" 

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

1166 

1167 if (projectURL := getattr(dunderModule, "__project_url__", None)) is not None: 

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

1169 

1170 if (documentationURL := getattr(dunderModule, "__documentation_url__", None)) is not None: 

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

1172 

1173 if (issueTrackerURL := getattr(dunderModule, "__issue_tracker_url__", None)) is not None: 

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

1175 

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

1177 """ 

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

1179 

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

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

1182 

1183 :param packageName: Name of the package on PyPI. 

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

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

1186 """ 

1187 from json import loads 

1188 from urllib.request import urlopen, Request 

1189 

1190 request = Request( 

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

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

1193 ) 

1194 try: 

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

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

1197 return data["info"]["version"] 

1198 except Exception: 

1199 return None 

1200 

1201 def Configure( 

1202 self, 

1203 *, 

1204 verbose: bool = False, 

1205 debug: bool = False, 

1206 silent: bool = False, 

1207 quiet: bool = False, 

1208 writeToStdOut: bool = True 

1209 ) -> None: 

1210 """ 

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

1212 

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

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

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

1216 

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

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

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

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

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

1222 """ 

1223 self._verbose = True if debug else verbose 

1224 self._debug = debug 

1225 self._silent = silent 

1226 self._quiet = quiet 

1227 

1228 if quiet: 1228 ↛ 1230line 1228 didn't jump to line 1230 because the condition on line 1228 was always true

1229 self._writeLevel = Severity.Quiet 

1230 elif silent: 

1231 self._writeLevel = Severity.Silent 

1232 elif debug: 

1233 self._writeLevel = Severity.Debug 

1234 elif verbose: 

1235 self._writeLevel = Severity.Verbose 

1236 else: 

1237 self._writeLevel = Severity.Normal 

1238 

1239 self._writeToStdOut = writeToStdOut 

1240 

1241 @readonly 

1242 def Verbose(self) -> bool: 

1243 """ 

1244 Check if verbose messages are enabled. 

1245 

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

1247 """ 

1248 return self._verbose 

1249 

1250 @readonly 

1251 def Debug(self) -> bool: 

1252 """ 

1253 Check if debug messages are enabled. 

1254 

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

1256 """ 

1257 return self._debug 

1258 

1259 @readonly 

1260 def Silent(self) -> bool: 

1261 """ 

1262 Check if silent mode is enabled. 

1263 

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

1265 """ 

1266 return self._silent 

1267 

1268 @readonly 

1269 def Quiet(self) -> bool: 

1270 """ 

1271 Check if quiet mode is enabled. 

1272 

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

1274 """ 

1275 return self._quiet 

1276 

1277 @property 

1278 def LogLevel(self) -> Severity: 

1279 """ 

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

1281 

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

1283 

1284 :returns: The current minimal severity level. 

1285 """ 

1286 return self._writeLevel 

1287 

1288 @LogLevel.setter 

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

1290 self._writeLevel = value 

1291 

1292 @property 

1293 def BaseIndent(self) -> int: 

1294 """ 

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

1296 

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

1298 

1299 :returns: Base indentation level. 

1300 """ 

1301 return self._baseIndent 

1302 

1303 @BaseIndent.setter 

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

1305 self._baseIndent = value 

1306 

1307 @readonly 

1308 def WarningCount(self) -> int: 

1309 """ 

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

1311 

1312 :returns: Number of warnings. 

1313 """ 

1314 return self._warningCount 

1315 

1316 @readonly 

1317 def CriticalWarningCount(self) -> int: 

1318 """ 

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

1320 

1321 :returns: Number of critical warnings. 

1322 """ 

1323 return self._criticalWarningCount 

1324 

1325 @readonly 

1326 def ErrorCount(self) -> int: 

1327 """ 

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

1329 

1330 :returns: Number of errors. 

1331 """ 

1332 return self._errorCount 

1333 

1334 @readonly 

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

1336 """ 

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

1338 

1339 :returns: List of lines. 

1340 """ 

1341 return self._lines 

1342 

1343 def ExitOnPreviousErrors(self) -> None: 

1344 """ 

1345 Exit application if errors have been printed. 

1346 """ 

1347 if self._errorCount > 0: 

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

1349 

1350 def ExitOnPreviousCriticalWarnings( 

1351 self, 

1352 includeErrors: bool = True 

1353 ) -> None: 

1354 """ 

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

1356 

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

1358 """ 

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

1360 if self._criticalWarningCount > 0: 

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

1362 else: 

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

1364 elif self._criticalWarningCount > 0: 

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

1366 

1367 def ExitOnPreviousWarnings( 

1368 self, 

1369 includeCriticalWarnings: bool = True, 

1370 includeErrors: bool = True 

1371 ) -> None: 

1372 """ 

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

1374 

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

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

1377 """ 

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

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

1380 if self._warningCount > 0: 

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

1382 else: 

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

1384 elif self._warningCount > 0: 

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

1386 else: 

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

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

1389 if self._warningCount > 0: 

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

1391 else: 

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

1393 elif self._warningCount > 0: 

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

1395 

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

1397 """ 

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

1399 

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

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

1402 

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

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

1405 """ 

1406 if line.Severity < self._writeLevel: 

1407 return False 

1408 

1409 self._lines.append(line) 

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

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

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

1413 

1414 return True 

1415 

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

1417 """ 

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

1419 

1420 :param line: Line object to check. 

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

1422 """ 

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

1424 return severity >= self._writeLevel 

1425 

1426 def WriteFatal( 

1427 self, 

1428 message: str, 

1429 *, 

1430 indent: int = 0, 

1431 appendLinebreak: bool = True, 

1432 exitCode: int = 0, 

1433 immediateExit: bool = True 

1434 ) -> bool: 

1435 """ 

1436 Write a fatal message and exit. 

1437 

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

1439 

1440 :param message: Message to write. 

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

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

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

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

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

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

1447 """ 

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

1449 if immediateExit: 

1450 self.FatalExit(exitCode) 

1451 return ret 

1452 

1453 def WriteError( 

1454 self, 

1455 message: str, 

1456 *, 

1457 indent: int = 0, 

1458 appendLinebreak: bool = True 

1459 ) -> bool: 

1460 """ 

1461 Write an error message. 

1462 

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

1464 

1465 :param message: Message to write. 

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

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

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

1469 """ 

1470 self._errorCount += 1 

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

1472 

1473 def WriteQuiet( 

1474 self, 

1475 message: str, 

1476 *, 

1477 indent: int = 0, 

1478 appendLinebreak: bool = True 

1479 ) -> bool: 

1480 """ 

1481 Write an always visible message. 

1482 

1483 This message is even visible in quiet mode. 

1484 

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

1486 

1487 :param message: Message to write. 

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

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

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

1491 """ 

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

1493 

1494 def WriteCritical( 

1495 self, 

1496 message: str, 

1497 *, 

1498 indent: int = 0, 

1499 appendLinebreak: bool = True 

1500 ) -> bool: 

1501 """ 

1502 Write a critical message. 

1503 

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

1505 

1506 :param message: Message to write. 

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

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

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

1510 """ 

1511 self._criticalWarningCount += 1 

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

1513 

1514 def WriteErrorNote( 

1515 self, 

1516 message: str, 

1517 *, 

1518 indent: int = 0, 

1519 appendLinebreak: bool = True 

1520 ) -> bool: 

1521 """ 

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

1523 

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

1525 

1526 :param message: Message to write. 

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

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

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

1530 """ 

1531 return self.WriteLine( 

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

1533 ) 

1534 

1535 def WriteCriticalNote( 

1536 self, 

1537 message: str, 

1538 *, 

1539 indent: int = 0, 

1540 appendLinebreak: bool = True 

1541 ) -> bool: 

1542 """ 

1543 Write a critical note. 

1544 

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

1546 

1547 :param message: Message to write. 

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

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

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

1551 """ 

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

1553 

1554 def WriteWarning( 

1555 self, 

1556 message: str, 

1557 *, 

1558 indent: int = 0, 

1559 appendLinebreak: bool = True 

1560 ) -> bool: 

1561 """ 

1562 Write a warning message. 

1563 

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

1565 

1566 :param message: Message to write. 

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

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

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

1570 """ 

1571 self._warningCount += 1 

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

1573 

1574 def WriteWarningNote( 

1575 self, 

1576 message: str, 

1577 *, 

1578 indent: int = 0, 

1579 appendLinebreak: bool = True 

1580 ) -> bool: 

1581 """ 

1582 Write a warning note. 

1583 

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

1585 

1586 :param message: Message to write. 

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

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

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

1590 """ 

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

1592 

1593 def WriteInfo( 

1594 self, 

1595 message: str, 

1596 *, 

1597 indent: int = 0, 

1598 appendLinebreak: bool = True 

1599 ) -> bool: 

1600 """ 

1601 Write an info message. 

1602 

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

1604 

1605 :param message: Message to write. 

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

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

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

1609 """ 

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

1611 

1612 def WriteNormal( 

1613 self, 

1614 message: str, 

1615 *, 

1616 indent: int = 0, 

1617 appendLinebreak: bool = True 

1618 ) -> bool: 

1619 """ 

1620 Write a normal message. 

1621 

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

1623 

1624 :param message: Message to write. 

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

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

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

1628 """ 

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

1630 

1631 def WriteVerbose( 

1632 self, 

1633 message: str, 

1634 *, 

1635 indent: int = 0, 

1636 appendLinebreak: bool = True 

1637 ) -> bool: 

1638 """ 

1639 Write a verbose message. 

1640 

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

1642 

1643 :param message: Message to write. 

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

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

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

1647 """ 

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

1649 

1650 def WriteDebug( 

1651 self, 

1652 message: str, 

1653 *, 

1654 indent: int = 0, 

1655 appendLinebreak: bool = True 

1656 ) -> bool: 

1657 """ 

1658 Write a debug message. 

1659 

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

1661 

1662 :param message: Message to write. 

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

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

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

1666 """ 

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

1668 

1669 def WriteDryRun( 

1670 self, 

1671 message: str, 

1672 *, 

1673 indent: int = 0, 

1674 appendLinebreak: bool = True 

1675 ) -> bool: 

1676 """ 

1677 Write a dry-run message message. 

1678 

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

1680 

1681 :param message: Message to write. 

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

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

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

1685 """ 

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

1687 

1688 

1689@export 

1690class TerminalApplicationProtocol(Protocol): 

1691 """ 

1692 The members a mixin-class can expect from a :class:`TerminalApplication` it is mixed into. 

1693 

1694 A mixin-class implementing command handlers writes messages, reads the verbosity and exits through its host class. It 

1695 names this protocol as its ``expects`` class keyword argument, so a class mixing it in without being a 

1696 :class:`TerminalApplication` can't be instantiated. Annotating ``self`` with the protocol lets a type-checker check 

1697 the calls inside the mixin-class: 

1698 

1699 .. code-block:: Python 

1700 

1701 class PipelineHandlers(metaclass=ExtendedType, mixin=True, expects=TerminalApplicationProtocol): 

1702 def HandlePipeline(self: TerminalApplicationProtocol, args: Namespace) -> None: 

1703 self._PrintHeadline() 

1704 self.WriteNormal("...") 

1705 

1706 .. seealso:: 

1707 

1708 :class:`~pyTooling.MetaClasses.ExtendedType` 

1709 |rarr| The ``expects`` class keyword argument of mixin-classes. 

1710 """ 

1711 

1712 @InheritDocString(TerminalApplication) 

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

1714 ... 

1715 

1716 @readonly 

1717 @InheritDocString(TerminalApplication) 

1718 def Width(self) -> int: 

1719 ... 

1720 

1721 @readonly 

1722 @InheritDocString(TerminalApplication) 

1723 def Verbose(self) -> bool: 

1724 ... 

1725 

1726 @readonly 

1727 @InheritDocString(TerminalApplication) 

1728 def Debug(self) -> bool: 

1729 ... 

1730 

1731 @readonly 

1732 @InheritDocString(TerminalApplication) 

1733 def Quiet(self) -> bool: 

1734 ... 

1735 

1736 @InheritDocString(TerminalApplication) 

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

1738 ... 

1739 

1740 @InheritDocString(TerminalApplication) 

1741 def ExitOnPreviousErrors(self) -> None: 

1742 ... 

1743 

1744 @InheritDocString(TerminalApplication) 

1745 def ExitOnPreviousCriticalWarnings( 

1746 self, 

1747 includeErrors: bool = True 

1748 ) -> None: 

1749 ... 

1750 

1751 @InheritDocString(TerminalApplication) 

1752 def ExitOnPreviousWarnings( 

1753 self, 

1754 includeCriticalWarnings: bool = True, 

1755 includeErrors: bool = True 

1756 ) -> None: 

1757 ... 

1758 

1759 @InheritDocString(TerminalApplication) 

1760 def WriteFatal( 

1761 self, 

1762 message: str, 

1763 *, 

1764 indent: int = 0, 

1765 appendLinebreak: bool = True, 

1766 exitCode: int = 0, 

1767 immediateExit: bool = True 

1768 ) -> bool: 

1769 ... 

1770 

1771 @InheritDocString(TerminalApplication) 

1772 def WriteError( 

1773 self, 

1774 message: str, 

1775 *, 

1776 indent: int = 0, 

1777 appendLinebreak: bool = True 

1778 ) -> bool: 

1779 ... 

1780 

1781 @InheritDocString(TerminalApplication) 

1782 def WriteErrorNote( 

1783 self, 

1784 message: str, 

1785 *, 

1786 indent: int = 0, 

1787 appendLinebreak: bool = True 

1788 ) -> bool: 

1789 ... 

1790 

1791 @InheritDocString(TerminalApplication) 

1792 def WriteCritical( 

1793 self, 

1794 message: str, 

1795 *, 

1796 indent: int = 0, 

1797 appendLinebreak: bool = True 

1798 ) -> bool: 

1799 ... 

1800 

1801 @InheritDocString(TerminalApplication) 

1802 def WriteCriticalNote( 

1803 self, 

1804 message: str, 

1805 *, 

1806 indent: int = 0, 

1807 appendLinebreak: bool = True 

1808 ) -> bool: 

1809 ... 

1810 

1811 @InheritDocString(TerminalApplication) 

1812 def WriteWarning( 

1813 self, 

1814 message: str, 

1815 *, 

1816 indent: int = 0, 

1817 appendLinebreak: bool = True 

1818 ) -> bool: 

1819 ... 

1820 

1821 @InheritDocString(TerminalApplication) 

1822 def WriteWarningNote( 

1823 self, 

1824 message: str, 

1825 *, 

1826 indent: int = 0, 

1827 appendLinebreak: bool = True 

1828 ) -> bool: 

1829 ... 

1830 

1831 @InheritDocString(TerminalApplication) 

1832 def WriteInfo( 

1833 self, 

1834 message: str, 

1835 *, 

1836 indent: int = 0, 

1837 appendLinebreak: bool = True 

1838 ) -> bool: 

1839 ... 

1840 

1841 @InheritDocString(TerminalApplication) 

1842 def WriteQuiet( 

1843 self, 

1844 message: str, 

1845 *, 

1846 indent: int = 0, 

1847 appendLinebreak: bool = True 

1848 ) -> bool: 

1849 ... 

1850 

1851 @InheritDocString(TerminalApplication) 

1852 def WriteNormal( 

1853 self, 

1854 message: str, 

1855 *, 

1856 indent: int = 0, 

1857 appendLinebreak: bool = True 

1858 ) -> bool: 

1859 ... 

1860 

1861 @InheritDocString(TerminalApplication) 

1862 def WriteVerbose( 

1863 self, 

1864 message: str, 

1865 *, 

1866 indent: int = 0, 

1867 appendLinebreak: bool = True 

1868 ) -> bool: 

1869 ... 

1870 

1871 @InheritDocString(TerminalApplication) 

1872 def WriteDebug( 

1873 self, 

1874 message: str, 

1875 *, 

1876 indent: int = 0, 

1877 appendLinebreak: bool = True 

1878 ) -> bool: 

1879 ... 

1880 

1881 @InheritDocString(TerminalApplication) 

1882 def WriteDryRun( 

1883 self, 

1884 message: str, 

1885 *, 

1886 indent: int = 0, 

1887 appendLinebreak: bool = True 

1888 ) -> bool: 

1889 ...