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

538 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-31 07:24 +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"""A set of helpers to implement a text user interface (TUI) in a terminal.""" 

33from datetime import datetime 

34from enum import Enum, unique 

35from io import TextIOWrapper 

36from sys import stdin, stdout, stderr 

37from textwrap import dedent 

38from types import ModuleType 

39from typing import NoReturn, Tuple, Any, List, Optional as Nullable, Dict, Callable, ClassVar 

40 

41from pyTooling.Versioning import PythonVersion 

42 

43try: 

44 from colorama import Fore as Foreground 

45except ImportError as ex: # pragma: no cover 

46 raise Exception(f"Optional dependency 'colorama' not installed. Either install pyTooling with extra dependencies 'pyTooling[terminal]' or install 'colorama' directly.") from ex 

47 

48from pyTooling.Decorators import export, readonly 

49from pyTooling.MetaClasses import ExtendedType, mixin 

50from pyTooling.Exceptions import PlatformNotSupportedException, ExceptionBase 

51from pyTooling.Common import lastItem 

52from pyTooling.Platform import Platform 

53 

54 

55@export 

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

57 """ 

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

59 

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

61 terminal's width. 

62 """ 

63 

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

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

66 PYTHON_VERSION_CHECK_FAILED_EXIT_CODE: ClassVar[int] = 254 #: Return code, if version check was not successful. 

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

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

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

70 

71 try: 

72 from colorama import Fore as Foreground 

73 Foreground: ClassVar[Dict[str, str]] = { 

74 "RED": Foreground.LIGHTRED_EX, 

75 "DARK_RED": Foreground.RED, 

76 "GREEN": Foreground.LIGHTGREEN_EX, 

77 "DARK_GREEN": Foreground.GREEN, 

78 "YELLOW": Foreground.LIGHTYELLOW_EX, 

79 "DARK_YELLOW": Foreground.YELLOW, 

80 "MAGENTA": Foreground.LIGHTMAGENTA_EX, 

81 "BLUE": Foreground.LIGHTBLUE_EX, 

82 "DARK_BLUE": Foreground.BLUE, 

83 "CYAN": Foreground.LIGHTCYAN_EX, 

84 "DARK_CYAN": Foreground.CYAN, 

85 "GRAY": Foreground.WHITE, 

86 "DARK_GRAY": Foreground.LIGHTBLACK_EX, 

87 "WHITE": Foreground.LIGHTWHITE_EX, 

88 "NOCOLOR": Foreground.RESET, 

89 

90 "HEADLINE": Foreground.LIGHTMAGENTA_EX, 

91 "ERROR": Foreground.LIGHTRED_EX, 

92 "WARNING": Foreground.LIGHTYELLOW_EX 

93 } #: Terminal colors 

94 except ImportError: # pragma: no cover 

95 Foreground: ClassVar[Dict[str, str]] = { 

96 "RED": "", 

97 "DARK_RED": "", 

98 "GREEN": "", 

99 "DARK_GREEN": "", 

100 "YELLOW": "", 

101 "DARK_YELLOW": "", 

102 "MAGENTA": "", 

103 "BLUE": "", 

104 "DARK_BLUE": "", 

105 "CYAN": "", 

106 "DARK_CYAN": "", 

107 "GRAY": "", 

108 "DARK_GRAY": "", 

109 "WHITE": "", 

110 "NOCOLOR": "", 

111 

112 "HEADLINE": "", 

113 "ERROR": "", 

114 "WARNING": "" 

115 } #: Terminal colors 

116 

117 _stdin: TextIOWrapper #: STDIN 

118 _stdout: TextIOWrapper #: STDOUT 

119 _stderr: TextIOWrapper #: STDERR 

120 _width: int #: Terminal width in characters 

121 _height: int #: Terminal height in characters 

122 

123 def __init__(self) -> None: 

124 """ 

125 Initialize a terminal. 

126 

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

128 it for colored outputs. 

129 

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

131 """ 

132 

133 self._stdin = stdin 

134 self._stdout = stdout 

135 self._stderr = stderr 

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

137 self.InitializeColors() 

138 else: 

139 self.UninitializeColors() 

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

141 

142 def InitializeColors(self) -> bool: 

143 """ 

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

145 

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

147 """ 

148 try: 

149 from colorama import init 

150 

151 init() 

152 return True 

153 except ImportError: # pragma: no cover 

154 return False 

155 

156 def UninitializeColors(self) -> bool: 

157 """ 

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

159 

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

161 """ 

162 try: 

163 from colorama import deinit 

164 

165 deinit() 

166 return True 

167 except ImportError: # pragma: no cover 

168 return False 

169 

170 @readonly 

171 def Width(self) -> int: 

172 """ 

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

174 

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

176 """ 

177 return self._width 

178 

179 @readonly 

180 def Height(self) -> int: 

181 """ 

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

183 

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

185 """ 

186 return self._height 

187 

188 @staticmethod 

189 def GetTerminalSize() -> Tuple[int, int]: 

190 """ 

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

192 

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

194 :raises PlatformNotSupportedException: When a platform is not yet supported. 

195 """ 

196 platform = Platform() 

197 if platform.IsNativeWindows: 

198 size = TerminalBaseApplication.__GetTerminalSizeOnWindows() 

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

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

201 size = TerminalBaseApplication.__GetTerminalSizeOnLinux() 

202 else: # pragma: no cover 

203 raise PlatformNotSupportedException(f"Platform '{platform}' not yet supported.") 

204 

205 if size is None: # pragma: no cover 

206 size = (80, 25) # default size 

207 

208 return size 

209 

210 @staticmethod 

211 def __GetTerminalSizeOnWindows() -> Nullable[Tuple[int, int]]: 

212 """ 

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

214 

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

216 

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

218 """ 

219 try: 

220 from ctypes import windll, create_string_buffer 

221 from struct import unpack as struct_unpack 

222 

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

224 stringBuffer = create_string_buffer(22) 

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

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

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

228 width = right - left + 1 

229 height = bottom - top + 1 

230 return width, height 

231 except ImportError: 

232 pass 

233 

234 return None 

235 # return Terminal.__GetTerminalSizeWithTPut() 

236 

237 # @staticmethod 

238 # def __GetTerminalSizeWithTPut() -> Tuple[int, int]: 

239 # """ 

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

241 # 

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

243 # 

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

245 # """ 

246 # from subprocess import check_output 

247 # 

248 # try: 

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

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

251 # return (width, height) 

252 # except: 

253 # pass 

254 

255 @staticmethod 

256 def __GetTerminalSizeOfFileDescriptor(fd: int) -> Nullable[Tuple[int, int]]: # Python 3.10: Use bitwise-or for union type: | None: 

257 """ 

258 Get window size of a file descriptor. 

259 

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

261 

262 :param fd: File descriptor to query. 

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

264 """ 

265 try: 

266 from array import array 

267 from fcntl import ioctl 

268 from termios import TIOCGWINSZ 

269 except ImportError: 

270 return None 

271 

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

273 # H = unsigned short (16-bit) 

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

275 try: 

276 ioctl(fd, TIOCGWINSZ, buffer, True) 

277 return buffer[1], buffer[0] 

278 except OSError: 

279 return None 

280 

281 @staticmethod 

282 def __GetTerminalSizeOnLinux() -> Nullable[Tuple[int, int]]: # Python 3.10: Use bitwise-or for union type: | None: 

283 """ 

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

285 

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

287 ``LINES`` are checked. 

288 

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

290 """ 

291 # STDIN, STDOUT, STDERR 

292 for fd in range(3): 

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

294 return size 

295 

296 # Fallback 

297 fd = None 

298 try: 

299 from os import open, close, ctermid, O_RDONLY 

300 

301 fd = open(ctermid(), O_RDONLY) 

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

303 return size 

304 except (ImportError, OSError): 

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

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

307 pass 

308 finally: 

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

310 try: 

311 close(fd) 

312 except OSError: 

313 pass 

314 

315 # Fall-fallback 

316 from os import getenv 

317 

318 try: 

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

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

321 return columns, lines 

322 except TypeError: 

323 pass 

324 

325 return None 

326 

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

328 """ 

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

330 

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

332 :returns: Number of written characters. 

333 """ 

334 return self._stdout.write(message) 

335 

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

337 """ 

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

339 

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

341 :param end: Use newline character. Default: ``\\n``. 

342 :returns: Number of written characters. 

343 """ 

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

345 

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

347 """ 

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

349 

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

351 :returns: Number of written characters. 

352 """ 

353 return self._stderr.write(message) 

354 

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

356 """ 

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

358 

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

360 :param end: Use newline character. Default: ``\\n``. 

361 :returns: Number of written characters. 

362 """ 

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

364 

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

366 """ 

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

368 

369 :param returnCode: Return code for application exit. 

370 """ 

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

372 

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

374 """ 

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

376 

377 :param returnCode: Return code for application exit. 

378 """ 

379 self.UninitializeColors() 

380 exit(returnCode) 

381 

382 def CheckPythonVersion(self, version: Tuple[int, ...]) -> None: 

383 """ 

384 Check if the used Python interpreter fulfills the minimum version requirements. 

385 """ 

386 from sys import version_info as info 

387 

388 if info < version: 

389 self.InitializeColors() 

390 

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

392 {{RED}}[ERROR]{{NOCOLOR}} Used Python interpreter ({info.major}.{info.minor}.{info.micro}-{info.releaselevel}) is to old. 

393 {{indent}}{{YELLOW}}Minimal required Python version is {version[0]}.{version[1]}.{version[2]}{{NOCOLOR}}\ 

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

395 

396 self.Exit(self.PYTHON_VERSION_CHECK_FAILED_EXIT_CODE) 

397 

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

399 """ 

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

401 

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

403 

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

405 """ 

406 from traceback import format_tb, walk_tb 

407 

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

409 filename = frame.f_code.co_filename 

410 funcName = frame.f_code.co_name 

411 

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

413 message += f"{{indent}}{{YELLOW}}Exception type:{{NOCOLOR}} {{DARK_RED}}{ex.__class__.__name__}{{NOCOLOR}}\n" 

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

415 

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

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

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

419 for note in iterator: 

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

421 

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

423 

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

425 message += f"{{indent2}}{{DARK_YELLOW}}Caused by ex. type:{{NOCOLOR}} {{DARK_RED}}{ex2.__class__.__name__}{{NOCOLOR}}\n" 

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

427 

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

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

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

431 for note in iterator: 

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

433 

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

435 for line in format_tb(ex.__traceback__): 

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

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

438 

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

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

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

442 

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

444 self.Exit(self.UNHANDLED_EXCEPTION_EXIT_CODE) 

445 

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

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

448 from traceback import walk_tb 

449 

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

451 filename = frame.f_code.co_filename 

452 funcName = frame.f_code.co_name 

453 

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

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

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

457 

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

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

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

461 for note in iterator: 

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

463 

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

465 

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

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

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

469 

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

471 self.Exit(self.NOT_IMPLEMENTED_EXCEPTION_EXIT_CODE) 

472 

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

474 """ 

475 Prints an exception of type :exc:`ExceptionBase` and its traceback. 

476 

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

478 

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

480 """ 

481 from traceback import print_tb, walk_tb 

482 

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

484 filename = frame.f_code.co_filename 

485 funcName = frame.f_code.co_name 

486 

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

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

489 {{indent}}{{YELLOW}}Exception type:{{NOCOLOR}} {{DARK_RED}}{ex.__class__.__name__}{{NOCOLOR}} 

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

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

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

493 

494 if ex.__cause__ is not None: 

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

496 {{indent2}}{{DARK_YELLOW}}Caused by ex. type:{{NOCOLOR}} {{DARK_RED}}{ex.__cause__.__class__.__name__}{{NOCOLOR}} 

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

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

499 

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

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

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

503 

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

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

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

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

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

509 

510 self.Exit(self.UNHANDLED_EXCEPTION_EXIT_CODE) 

511 

512 

513@export 

514@unique 

515class Severity(Enum): 

516 """Logging message severity levels.""" 

517 

518 Exception = 120 #: Unhandled exception messages 

519 ExceptionCause = 115 #: Exception cause 

520 ExceptionNote = 110 #: Exception notes 

521 Fatal = 100 #: Fatal messages 

522 Error = 80 #: Error messages 

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

524 

525 Critical = 60 #: Critical messages 

526 CriticalNote = 55 #: Critical notes 

527 Warning = 50 #: Warning messages 

528 WarningNote = 45 #: Warning notes 

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

530 

531 Info = 20 #: Informative messages 

532 Normal = 10 #: Normal messages 

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

534 Verbose = 5 #: Verbose messages 

535 Debug = 2 #: Debug messages 

536 All = 0 #: All messages 

537 

538 def __hash__(self) -> int: 

539 return hash(self.name) 

540 

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

542 """ 

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

544 

545 :param other: Operand to compare against. 

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

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

548 """ 

549 if isinstance(other, Severity): 

550 return self.value == other.value 

551 else: 

552 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.") 

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

554 raise ex 

555 

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

557 """ 

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

559 

560 :param other: Operand to compare against. 

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

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

563 """ 

564 if isinstance(other, Severity): 

565 return self.value != other.value 

566 else: 

567 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by != operator.") 

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

569 raise ex 

570 

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

572 """ 

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

574 

575 :param other: Operand to compare against. 

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

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

578 """ 

579 if isinstance(other, Severity): 

580 return self.value < other.value 

581 else: 

582 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by < operator.") 

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

584 raise ex 

585 

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

587 """ 

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

589 

590 :param other: Operand to compare against. 

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

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

593 """ 

594 if isinstance(other, Severity): 

595 return self.value <= other.value 

596 else: 

597 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by <= operator.") 

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

599 raise ex 

600 

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

602 """ 

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

604 

605 :param other: Operand to compare against. 

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

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

608 """ 

609 if isinstance(other, Severity): 

610 return self.value > other.value 

611 else: 

612 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by > operator.") 

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

614 raise ex 

615 

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

617 """ 

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

619 

620 :param other: Operand to compare against. 

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

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(f"Second operand of type '{other.__class__.__name__}' is not supported by >= operator.") 

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

629 raise ex 

630 

631 

632@export 

633@unique 

634class Mode(Enum): 

635 TextToStdOut_ErrorsToStdErr = 0 

636 AllLinearToStdOut = 1 

637 DataToStdOut_OtherToStdErr = 2 

638 

639 

640@export 

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

642 """ 

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

644 """ 

645 

646 _LOG_MESSAGE_FORMAT__: ClassVar[Dict[Severity, str]] = { 

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

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

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

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

651 Severity.Quiet: "{message}", 

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

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

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

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

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

657 Severity.Normal: "{message}", 

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

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

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

661 } #: Message line formatting rules. 

662 

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

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

665 _severity: Severity #: Message severity 

666 _indent: int #: Indentation 

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

668 

669 def __init__( 

670 self, 

671 message: str, 

672 severity: Severity = Severity.Normal, 

673 *, 

674 indent: int = 0, 

675 appendLinebreak: bool = True 

676 ) -> None: 

677 """ 

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

679 

680 :param message: Message to display. 

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

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

683 :param appendLinebreak: Optional, append a line break at the end of the message. 

684 """ 

685 self._timestamp = datetime.now() 

686 self._severity = severity 

687 self._message = message 

688 self._indent = indent 

689 self._appendLinebreak = appendLinebreak 

690 

691 @readonly 

692 def Message(self) -> str: 

693 """ 

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

695 

696 :returns: Raw message of the line. 

697 """ 

698 return self._message 

699 

700 @readonly 

701 def Severity(self) -> Severity: 

702 """ 

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

704 

705 :returns: Severity level of the message line. 

706 """ 

707 return self._severity 

708 

709 @readonly 

710 def Indent(self) -> int: 

711 """ 

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

713 

714 :returns: Indentation level of the message line. 

715 """ 

716 return self._indent 

717 

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

719 """ 

720 Increase a line's indentation level. 

721 

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

723 """ 

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

725 return newIndent 

726 

727 @readonly 

728 def AppendLinebreak(self) -> bool: 

729 """ 

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

731 

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

733 """ 

734 return self._appendLinebreak 

735 

736 def __str__(self) -> str: 

737 """ 

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

739 

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

741 

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

743 """ 

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

745 

746 

747@export 

748@mixin 

749class ILineTerminal: 

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

751 

752 _terminal: TerminalBaseApplication 

753 

754 def __init__(self, terminal: Nullable[TerminalBaseApplication] = None) -> None: 

755 """MixIn initializer.""" 

756 self._terminal = terminal 

757 

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

759 

760 @readonly 

761 def Terminal(self) -> TerminalBaseApplication: 

762 """ 

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

764 

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

766 """ 

767 return self._terminal 

768 

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

770 """Write an entry to the local terminal.""" 

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

772 return self._terminal.WriteLine(line) 

773 return False 

774 

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

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

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

778 # return False 

779 

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

781 """Write a fatal message if ``condition`` is true.""" 

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

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

784 return False 

785 

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

787 """Write an error message if ``condition`` is true.""" 

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

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

790 return False 

791 

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

793 """Write a warning message if ``condition`` is true.""" 

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

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

796 return False 

797 

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

799 """Write a warning message if ``condition`` is true.""" 

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

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

802 return False 

803 

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

805 """Write an info message if ``condition`` is true.""" 

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

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

808 return False 

809 

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

811 """Write a message even in quiet mode if ``condition`` is true.""" 

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

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

814 return False 

815 

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

817 """Write a *normal* message if ``condition`` is true.""" 

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

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

820 return False 

821 

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

823 """Write a verbose message if ``condition`` is true.""" 

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

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

826 return False 

827 

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

829 """Write a debug message if ``condition`` is true.""" 

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

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

832 return False 

833 

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

835 """Write a dry-run message if ``condition`` is true.""" 

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

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

838 return False 

839 

840 

841@export 

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

843 """ 

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

845 """ 

846 _LOG_MESSAGE_FORMAT__: ClassVar[Dict[Severity, str]] = { 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

861 } #: Message formatting rules. 

862 

863 _LOG_LEVEL_ROUTING__: Dict[Severity, Tuple[Callable[[str, str], int]]] #: Message routing rules. 

864 _verbose: bool 

865 _debug: bool 

866 _silent: bool 

867 _quiet: bool 

868 _writeLevel: Severity 

869 _writeToStdOut: bool 

870 

871 _lines: List[Line] 

872 _baseIndent: int 

873 

874 _errorCount: int 

875 _criticalWarningCount: int 

876 _warningCount: int 

877 

878 HeadLine: ClassVar[str] 

879 

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

881 """ 

882 Initializer of a line-based terminal interface. 

883 

884 :param mode: Defines what output (normal, error, data) to write where. Default: a linear flow all to *STDOUT*. 

885 """ 

886 TerminalBaseApplication.__init__(self) 

887 # ILineTerminal.__init__(self, self) 

888 

889 self._LOG_LEVEL_ROUTING__ = {} 

890 self.__InitializeLogLevelRouting(mode) 

891 

892 self._verbose = False 

893 self._debug = False 

894 self._silent = False 

895 self._quiet = False 

896 self._writeLevel = Severity.Normal 

897 self._writeToStdOut = True 

898 

899 self._lines = [] 

900 self._baseIndent = 0 

901 

902 self._errorCount = 0 

903 self._criticalWarningCount = 0 

904 self._warningCount = 0 

905 

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

907 if mode is Mode.TextToStdOut_ErrorsToStdErr: 

908 for severity in Severity: 

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

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

911 else: 

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

913 elif mode is Mode.AllLinearToStdOut: 

914 for severity in Severity: 

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

916 elif mode is Mode.DataToStdOut_OtherToStdErr: 

917 for severity in Severity: 

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

919 else: # pragma: no cover 

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

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

922 raise ex 

923 

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

925 """ 

926 Helper method to print the program headline. 

927 

928 :param width: Number of characters for horizontal lines. 

929 

930 .. admonition:: Generated output 

931 

932 .. code-block:: 

933 

934 ========================= 

935 centered headline 

936 ========================= 

937 """ 

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

939 width = self._width 

940 

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

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

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

944 

945 def _PrintHelp(self, command: Nullable[str] = None) -> None: 

946 """ 

947 Helper function to print the command line parsers help page(s). 

948 

949 :param command: The subcommand to print the help page(s) for. 

950 """ 

951 if command is None: 

952 self.MainParser.print_help() 

953 elif command == "help": 

954 self.WriteWarning("This is a recursion ...") 

955 else: 

956 try: 

957 self.SubParsers[command].print_help() 

958 except KeyError: 

959 self.WriteError(f"Command {command} is unknown.") 

960 

961 def _PrintVersion( 

962 self, 

963 dunderModule: ModuleType, 

964 packageName: Nullable[str] = None, 

965 versionCheckTimeout: int = 1 

966 ) -> None: 

967 """ 

968 Helper method to print the version information. 

969 

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

971 

972 .. admonition:: Example usage 

973 

974 .. code-block:: Python 

975 

976 def _PrintVersion(self): 

977 import myPackage.MyModule as DunderModule 

978 

979 super()._PrintVersion( 

980 DunderModule, 

981 "MyModule" 

982 ) 

983 """ 

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

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

986 for copyright in copyrights[1:]: 

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

988 

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

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

991 

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

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

994 for author in authors[1:]: 

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

996 

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

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

999 

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

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

1002 else: 

1003 currentVersion = PythonVersion.Parse(version) 

1004 if packageName is None: 

1005 update = "" 

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

1007 latestVersion = PythonVersion.Parse(pypiVersion) 

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

1009 else: 

1010 update = " (PyPI timeout)" 

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

1012 

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

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

1015 

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

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

1018 

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

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

1021 

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

1023 from json import loads 

1024 from urllib.request import urlopen, Request 

1025 

1026 request = Request( 

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

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

1029 ) 

1030 try: 

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

1032 data = loads(response.read().decode()) 

1033 return data["info"]["version"] 

1034 except Exception: 

1035 return None 

1036 

1037 def Configure( 

1038 self, 

1039 *, 

1040 verbose: bool = False, 

1041 debug: bool = False, 

1042 silent: bool = False, 

1043 quiet: bool = False, 

1044 writeToStdOut: bool = True 

1045 ) -> None: 

1046 self._verbose = True if debug else verbose 

1047 self._debug = debug 

1048 self._silent = silent 

1049 self._quiet = quiet 

1050 

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

1052 self._writeLevel = Severity.Quiet 

1053 elif silent: 

1054 self._writeLevel = Severity.Silent 

1055 elif debug: 

1056 self._writeLevel = Severity.Debug 

1057 elif verbose: 

1058 self._writeLevel = Severity.Verbose 

1059 else: 

1060 self._writeLevel = Severity.Normal 

1061 

1062 self._writeToStdOut = writeToStdOut 

1063 

1064 @readonly 

1065 def Verbose(self) -> bool: 

1066 """ 

1067 Check if verbose messages are enabled. 

1068 

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

1070 """ 

1071 return self._verbose 

1072 

1073 @readonly 

1074 def Debug(self) -> bool: 

1075 """ 

1076 Check if debug messages are enabled. 

1077 

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

1079 """ 

1080 return self._debug 

1081 

1082 @readonly 

1083 def Silent(self) -> bool: 

1084 """ 

1085 Check if silent mode is enabled. 

1086 

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

1088 """ 

1089 return self._silent 

1090 

1091 @readonly 

1092 def Quiet(self) -> bool: 

1093 """ 

1094 Check if quiet mode is enabled. 

1095 

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

1097 """ 

1098 return self._quiet 

1099 

1100 @property 

1101 def LogLevel(self) -> Severity: 

1102 """ 

1103 Read-only property to access the minimal severity level a message needs to be written (:attr:`_writeLevel`). 

1104 

1105 :returns: The current minimal severity level. 

1106 """ 

1107 return self._writeLevel 

1108 

1109 @LogLevel.setter 

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

1111 """Set the minimal severity level for writing.""" 

1112 self._writeLevel = value 

1113 

1114 @property 

1115 def BaseIndent(self) -> int: 

1116 """ 

1117 Read-only property to access the base indentation level of written messages (:attr:`_baseIndent`). 

1118 

1119 :returns: Base indentation level. 

1120 """ 

1121 return self._baseIndent 

1122 

1123 @BaseIndent.setter 

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

1125 self._baseIndent = value 

1126 

1127 @readonly 

1128 def WarningCount(self) -> int: 

1129 """ 

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

1131 

1132 :returns: Number of warnings. 

1133 """ 

1134 return self._warningCount 

1135 

1136 @readonly 

1137 def CriticalWarningCount(self) -> int: 

1138 """ 

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

1140 

1141 :returns: Number of critical warnings. 

1142 """ 

1143 return self._criticalWarningCount 

1144 

1145 @readonly 

1146 def ErrorCount(self) -> int: 

1147 """ 

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

1149 

1150 :returns: Number of errors. 

1151 """ 

1152 return self._errorCount 

1153 

1154 @readonly 

1155 def Lines(self) -> List[Line]: 

1156 """ 

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

1158 

1159 :returns: List of lines. 

1160 """ 

1161 return self._lines 

1162 

1163 def ExitOnPreviousErrors(self) -> None: 

1164 """ 

1165 Exit application if errors have been printed. 

1166 """ 

1167 if self._errorCount > 0: 

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

1169 

1170 def ExitOnPreviousCriticalWarnings( 

1171 self, 

1172 includeErrors: bool = True 

1173 ) -> None: 

1174 """ 

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

1176 

1177 :param includeErrors: Include critical warning counts. 

1178 """ 

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

1180 if self._criticalWarningCount > 0: 

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

1182 else: 

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

1184 elif self._criticalWarningCount > 0: 

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

1186 

1187 def ExitOnPreviousWarnings( 

1188 self, 

1189 includeCriticalWarnings: bool = True, 

1190 includeErrors: bool = True 

1191 ) -> None: 

1192 """ 

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

1194 

1195 :param includeCriticalWarnings: Include critical warning counts. 

1196 :param includeErrors: Include error counts. 

1197 """ 

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

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

1200 if self._warningCount > 0: 

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

1202 else: 

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

1204 elif self._warningCount > 0: 

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

1206 else: 

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

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

1209 if self._warningCount > 0: 

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

1211 else: 

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

1213 elif self._warningCount > 0: 

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

1215 

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

1217 """ 

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

1219 

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

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

1222 """ 

1223 if line.Severity < self._writeLevel: 

1224 return False 

1225 

1226 self._lines.append(line) 

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

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

1229 

1230 return True 

1231 

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

1233 """ 

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

1235 

1236 :param line: Line object to check. 

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

1238 """ 

1239 return line.Severity >= self._writeLevel 

1240 

1241 def WriteFatal( 

1242 self, 

1243 message: str, 

1244 *, 

1245 indent: int = 0, 

1246 appendLinebreak: bool = True, 

1247 exitCode: int = 0, 

1248 immediateExit: bool = True 

1249 ) -> bool: 

1250 """ 

1251 Write a fatal message and exit. 

1252 

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

1254 

1255 :param message: Message to write. 

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

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

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

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

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

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

1262 """ 

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

1264 if immediateExit: 

1265 self.FatalExit(exitCode) 

1266 return ret 

1267 

1268 def WriteError( 

1269 self, 

1270 message: str, 

1271 *, 

1272 indent: int = 0, 

1273 appendLinebreak: bool = True 

1274 ) -> bool: 

1275 """ 

1276 Write an error message. 

1277 

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

1279 

1280 :param message: Message to write. 

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

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

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

1284 """ 

1285 self._errorCount += 1 

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

1287 

1288 def WriteQuiet( 

1289 self, 

1290 message: str, 

1291 *, 

1292 indent: int = 0, 

1293 appendLinebreak: bool = True 

1294 ) -> bool: 

1295 """ 

1296 Write an always visible message. 

1297 

1298 This message is even visible in quiet mode. 

1299 

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

1301 

1302 :param message: Message to write. 

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

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

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

1306 """ 

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

1308 

1309 def WriteCritical( 

1310 self, 

1311 message: str, 

1312 *, 

1313 indent: int = 0, 

1314 appendLinebreak: bool = True 

1315 ) -> bool: 

1316 """ 

1317 Write a critical message. 

1318 

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

1320 

1321 :param message: Message to write. 

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

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

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

1325 """ 

1326 self._criticalWarningCount += 1 

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

1328 

1329 def WriteCriticalNote( 

1330 self, 

1331 message: str, 

1332 *, 

1333 indent: int = 0, 

1334 appendLinebreak: bool = True 

1335 ) -> bool: 

1336 """ 

1337 Write a critical note. 

1338 

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

1340 

1341 :param message: Message to write. 

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

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

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

1345 """ 

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

1347 

1348 def WriteWarning( 

1349 self, 

1350 message: str, 

1351 *, 

1352 indent: int = 0, 

1353 appendLinebreak: bool = True 

1354 ) -> bool: 

1355 """ 

1356 Write a warning message. 

1357 

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

1359 

1360 :param message: Message to write. 

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

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

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

1364 """ 

1365 self._warningCount += 1 

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

1367 

1368 def WriteWarningNote( 

1369 self, 

1370 message: str, 

1371 *, 

1372 indent: int = 0, 

1373 appendLinebreak: bool = True 

1374 ) -> bool: 

1375 """ 

1376 Write a warning note. 

1377 

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

1379 

1380 :param message: Message to write. 

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

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

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

1384 """ 

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

1386 

1387 def WriteInfo( 

1388 self, 

1389 message: str, 

1390 *, 

1391 indent: int = 0, 

1392 appendLinebreak: bool = True 

1393 ) -> bool: 

1394 """ 

1395 Write an info message. 

1396 

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

1398 

1399 :param message: Message to write. 

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

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

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

1403 """ 

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

1405 

1406 def WriteNormal( 

1407 self, 

1408 message: str, 

1409 *, 

1410 indent: int = 0, 

1411 appendLinebreak: bool = True 

1412 ) -> bool: 

1413 """ 

1414 Write a normal message. 

1415 

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

1417 

1418 :param message: Message to write. 

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

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

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

1422 """ 

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

1424 

1425 def WriteVerbose( 

1426 self, 

1427 message: str, 

1428 *, 

1429 indent: int = 0, 

1430 appendLinebreak: bool = True 

1431 ) -> bool: 

1432 """ 

1433 Write a verbose message. 

1434 

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

1436 

1437 :param message: Message to write. 

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

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

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

1441 """ 

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

1443 

1444 def WriteDebug( 

1445 self, 

1446 message: str, 

1447 *, 

1448 indent: int = 0, 

1449 appendLinebreak: bool = True 

1450 ) -> bool: 

1451 """ 

1452 Write a debug message. 

1453 

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

1455 

1456 :param message: Message to write. 

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

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

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

1460 """ 

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

1462 

1463 def WriteDryRun( 

1464 self, 

1465 message: str, 

1466 *, 

1467 indent: int = 0, 

1468 appendLinebreak: bool = True 

1469 ) -> bool: 

1470 """ 

1471 Write a dry-run message message. 

1472 

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

1474 

1475 :param message: Message to write. 

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

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

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

1479 """ 

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