Coverage for pyTooling/TerminalUI/__init__.py: 70%
538 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-08 06:31 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-08 06:31 +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
41from pyTooling.Versioning import PythonVersion
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
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
55@export
56class TerminalBaseApplication(metaclass=ExtendedType, slots=True, singleton=True):
57 """
58 The class offers a basic terminal application base-class.
60 It offers basic colored output via `colorama <https://GitHub.com/tartley/colorama>`__ as well as retrieving the
61 terminal's width.
62 """
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)
71 try:
72 from colorama import Fore as Foreground
73 Foreground = {
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,
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 = {
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": "",
112 "HEADLINE": "",
113 "ERROR": "",
114 "WARNING": ""
115 } #: Terminal colors
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
123 def __init__(self) -> None:
124 """
125 Initialize a terminal.
127 If the Python package `colorama <https://pypi.org/project/colorama/>`_ [#f_colorama]_ is available, then initialize
128 it for colored outputs.
130 .. [#f_colorama] Colorama on Github: https://GitHub.com/tartley/colorama
131 """
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()
142 def InitializeColors(self) -> bool:
143 """
144 Initialize the terminal for color support by `colorama <https://GitHub.com/tartley/colorama>`__.
146 :returns: True, if 'colorama' package could be imported and initialized.
147 """
148 try:
149 from colorama import init
151 init()
152 return True
153 except ImportError: # pragma: no cover
154 return False
156 def UninitializeColors(self) -> bool:
157 """
158 Uninitialize the terminal for color support by `colorama <https://GitHub.com/tartley/colorama>`__.
160 :returns: True, if 'colorama' package could be imported and uninitialized.
161 """
162 try:
163 from colorama import deinit
165 deinit()
166 return True
167 except ImportError: # pragma: no cover
168 return False
170 @readonly
171 def Width(self) -> int:
172 """
173 Read-only property to access the terminal's width.
175 :returns: The terminal window's width in characters.
176 """
177 return self._width
179 @readonly
180 def Height(self) -> int:
181 """
182 Read-only property to access the terminal's height.
184 :returns: The terminal window's height in characters.
185 """
186 return self._height
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).
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.")
205 if size is None: # pragma: no cover
206 size = (80, 25) # default size
208 return size
210 @staticmethod
211 def __GetTerminalSizeOnWindows() -> Nullable[Tuple[int, int]]:
212 """
213 Returns the current terminal window's size for Windows.
215 ``kernel32.dll:GetConsoleScreenBufferInfo()`` is used to retrieve the information.
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
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
234 return None
235 # return Terminal.__GetTerminalSizeWithTPut()
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
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.
260 Call `ioctl` with ``TIOCGWINSZ`` (GetWindowsSize) for the given file descriptor.
262 :param fd: File descriptor
263 :return:
264 """
265 try:
266 from array import array
267 from fcntl import ioctl
268 from termios import TIOCGWINSZ
269 except ImportError:
270 return None
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
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.
286 ``ioctl(TIOCGWINSZ)`` is used to retrieve the information. As a fallback, environment variables ``COLUMNS`` and
287 ``LINES`` are checked.
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
296 # Fallback
297 fd = None
298 try:
299 from os import open, close, ctermid, O_RDONLY
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
315 # Fall-fallback
316 from os import getenv
318 try:
319 columns = int(getenv("COLUMNS"))
320 lines = int(getenv("LINES"))
321 return columns, lines
322 except TypeError:
323 pass
325 return None
327 def WriteToStdOut(self, message: str) -> int:
328 """
329 Low-level method for writing to ``STDOUT``.
331 :param message: Message to write to ``STDOUT``.
332 :returns: Number of written characters.
333 """
334 return self._stdout.write(message)
336 def WriteLineToStdOut(self, message: str, end: str = "\n") -> int:
337 """
338 Low-level method for writing to ``STDOUT``.
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)
346 def WriteToStdErr(self, message: str) -> int:
347 """
348 Low-level method for writing to ``STDERR``.
350 :param message: Message to write to ``STDERR``.
351 :returns: Number of written characters.
352 """
353 return self._stderr.write(message)
355 def WriteLineToStdErr(self, message: str, end: str = "\n") -> int:
356 """
357 Low-level method for writing to ``STDERR``.
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)
365 def FatalExit(self, returnCode: int = 0) -> NoReturn:
366 """
367 Exit the terminal application by uninitializing color support and returning a fatal Exit code.
369 :param returnCode: Return code for application exit.
370 """
371 self.Exit(self.FATAL_EXIT_CODE if returnCode == 0 else returnCode)
373 def Exit(self, returnCode: int = 0) -> NoReturn:
374 """
375 Exit the terminal application by uninitializing color support and returning an Exit code.
377 :param returnCode: Return code for application exit.
378 """
379 self.UninitializeColors()
380 exit(returnCode)
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
388 if info < version:
389 self.InitializeColors()
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))
396 self.Exit(self.PYTHON_VERSION_CHECK_FAILED_EXIT_CODE)
398 def PrintException(self, ex: Exception) -> NoReturn:
399 """
400 Prints an exception of type :exc:`Exception` and its traceback.
402 If the exception as a nested action, the cause is printed as well.
404 If ``ISSUE_TRACKER_URL`` is configured, a URL to the issue tracker is added.
405 """
406 from traceback import format_tb, walk_tb
408 frame, sourceLine = lastItem(walk_tb(ex.__traceback__))
409 filename = frame.f_code.co_filename
410 funcName = frame.f_code.co_name
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"
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"
422 message += f"{{indent}}{{YELLOW}}Caused in:{{NOCOLOR}} {funcName}(...) in file '{filename}' at line {sourceLine}\n"
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"
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"
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}}"
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}}"
443 self.WriteLineToStdErr(message.format(indent=self.INDENT, indent2=self.INDENT*2, **self.Foreground))
444 self.Exit(self.UNHANDLED_EXCEPTION_EXIT_CODE)
446 def PrintNotImplementedError(self, ex: NotImplementedError) -> NoReturn:
447 """Prints a not-implemented exception of type :exc:`NotImplementedError`."""
448 from traceback import walk_tb
450 frame, sourceLine = lastItem(walk_tb(ex.__traceback__))
451 filename = frame.f_code.co_filename
452 funcName = frame.f_code.co_name
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"
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"
464 message += f"{{indent}}{{YELLOW}}Caused in:{{NOCOLOR}} {funcName}(...) in file '{filename}' at line {sourceLine}\n"
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}}"
470 self.WriteLineToStdErr(message.format(indent=self.INDENT, indent2=self.INDENT * 2, **self.Foreground))
471 self.Exit(self.NOT_IMPLEMENTED_EXCEPTION_EXIT_CODE)
473 def PrintExceptionBase(self, ex: Exception) -> NoReturn:
474 """
475 Prints an exception of type :exc:`ExceptionBase` and its traceback.
477 If the exception as a nested action, the cause is printed as well.
479 If ``ISSUE_TRACKER_URL`` is configured, a URL to the issue tracker is added.
480 """
481 from traceback import print_tb, walk_tb
483 frame, sourceLine = lastItem(walk_tb(ex.__traceback__))
484 filename = frame.f_code.co_filename
485 funcName = frame.f_code.co_name
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))
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))
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))
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))
510 self.Exit(self.UNHANDLED_EXCEPTION_EXIT_CODE)
513@export
514@unique
515class Severity(Enum):
516 """Logging message severity levels."""
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.
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.
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
538 def __hash__(self) -> int:
539 return hash(self.name)
541 def __eq__(self, other: Any) -> bool:
542 """
543 Compare two Severity instances (severity level) for equality.
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
556 def __ne__(self, other: Any) -> bool:
557 """
558 Compare two Severity instances (severity level) for inequality.
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
571 def __lt__(self, other: Any) -> bool:
572 """
573 Compare two Severity instances (severity level) for less-than.
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
586 def __le__(self, other: Any) -> bool:
587 """
588 Compare two Severity instances (severity level) for less-than-or-equal.
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
601 def __gt__(self, other: Any) -> bool:
602 """
603 Compare two Severity instances (severity level) for greater-than.
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
616 def __ge__(self, other: Any) -> bool:
617 """
618 Compare two Severity instances (severity level) for greater-than-or-equal.
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
632@export
633@unique
634class Mode(Enum):
635 TextToStdOut_ErrorsToStdErr = 0
636 AllLinearToStdOut = 1
637 DataToStdOut_OtherToStdErr = 2
640@export
641class Line(metaclass=ExtendedType, slots=True):
642 """
643 Represents a single message line with a severity and indentation level.
644 """
646 _LOG_MESSAGE_FORMAT__ = {
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.
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.
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.
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
691 @readonly
692 def Message(self) -> str:
693 """
694 Read-only property to access the line's raw message.
696 :returns: Raw message of the line.
697 """
698 return self._message
700 @readonly
701 def Severity(self) -> Severity:
702 """
703 Read-only property to access the line's severity level.
705 :returns: Severity level of the message line.
706 """
707 return self._severity
709 @readonly
710 def Indent(self) -> int:
711 """
712 Read-only property to access the line's indentation level.
714 :returns: Indentation level of the message line.
715 """
716 return self._indent
718 def IndentBy(self, indent: int) -> int:
719 """
720 Increase a line's indentation level.
722 :param indent: Indentation level added to the current indentation level.
723 """
724 self._indent = (newIndent := self._indent + indent)
725 return newIndent
727 @readonly
728 def AppendLinebreak(self) -> bool:
729 """
730 Read-only property to return if a linebreak is added after the line's message.
732 :returns: True, if a linebreak should be added.
733 """
734 return self._appendLinebreak
736 def __str__(self) -> str:
737 """
738 Returns a formatted version of a ``Line`` objects as a string.
740 The formatting is defined in :attr:`_LOG_MESSAGE_FORMAT__`.
742 :returns: Formatted version of a ``Line`` object.
743 """
744 return self._LOG_MESSAGE_FORMAT__[self._severity].format(message=self._message)
747@export
748@mixin
749class ILineTerminal:
750 """A mixin class (interface) to provide class-local terminal writing methods."""
752 _terminal: TerminalBaseApplication
754 def __init__(self, terminal: Nullable[TerminalBaseApplication] = None) -> None:
755 """MixIn initializer."""
756 self._terminal = terminal
758 # FIXME: Alter methods if a terminal is present or set dummy methods
760 @readonly
761 def Terminal(self) -> TerminalBaseApplication:
762 """Return the local terminal instance."""
763 return self._terminal
765 def WriteLine(self, line: Line, condition: bool = True) -> bool:
766 """Write an entry to the local terminal."""
767 if (self._terminal is not None) and condition:
768 return self._terminal.WriteLine(line)
769 return False
771 # def _TryWriteLine(self, *args: Any, condition: bool = True, **kwargs: Any):
772 # if (self._terminal is not None) and condition:
773 # return self._terminal.TryWrite(*args, **kwargs)
774 # return False
776 def WriteFatal(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool:
777 """Write a fatal message if ``condition`` is true."""
778 if (self._terminal is not None) and condition:
779 return self._terminal.WriteFatal(*args, **kwargs)
780 return False
782 def WriteError(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool:
783 """Write an error message if ``condition`` is true."""
784 if (self._terminal is not None) and condition:
785 return self._terminal.WriteError(*args, **kwargs)
786 return False
788 def WriteCritical(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool:
789 """Write a warning message if ``condition`` is true."""
790 if (self._terminal is not None) and condition:
791 return self._terminal.WriteCritical(*args, **kwargs)
792 return False
794 def WriteWarning(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool:
795 """Write a warning message if ``condition`` is true."""
796 if (self._terminal is not None) and condition:
797 return self._terminal.WriteWarning(*args, **kwargs)
798 return False
800 def WriteInfo(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool:
801 """Write an info message if ``condition`` is true."""
802 if (self._terminal is not None) and condition:
803 return self._terminal.WriteInfo(*args, **kwargs)
804 return False
806 def WriteQuiet(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool:
807 """Write a message even in quiet mode if ``condition`` is true."""
808 if (self._terminal is not None) and condition:
809 return self._terminal.WriteQuiet(*args, **kwargs)
810 return False
812 def WriteNormal(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool:
813 """Write a *normal* message if ``condition`` is true."""
814 if (self._terminal is not None) and condition:
815 return self._terminal.WriteNormal(*args, **kwargs)
816 return False
818 def WriteVerbose(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool:
819 """Write a verbose message if ``condition`` is true."""
820 if (self._terminal is not None) and condition:
821 return self._terminal.WriteVerbose(*args, **kwargs)
822 return False
824 def WriteDebug(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool:
825 """Write a debug message if ``condition`` is true."""
826 if (self._terminal is not None) and condition:
827 return self._terminal.WriteDebug(*args, **kwargs)
828 return False
830 def WriteDryRun(self, *args: Any, condition: bool = True, **kwargs: Any) -> bool:
831 """Write a dry-run message if ``condition`` is true."""
832 if (self._terminal is not None) and condition:
833 return self._terminal.WriteDryRun(*args, **kwargs)
834 return False
837@export
838class TerminalApplication(TerminalBaseApplication): #, ILineTerminal):
839 """
840 A base-class for implementation of terminal applications emitting line-by-line messages.
841 """
842 _LOG_MESSAGE_FORMAT__ = {
843 Severity.Exception: "{RED}[EXCEPTION] {message}{NOCOLOR}",
844 Severity.ExceptionNote: "{DARK_RED} > {message}{NOCOLOR}",
845 Severity.Fatal: "{DARK_RED}[FATAL] {message}{NOCOLOR}",
846 Severity.Error: "{RED}[ERROR] {message}{NOCOLOR}",
847 Severity.Quiet: "{WHITE}{message}{NOCOLOR}",
848 Severity.Critical: "{DARK_YELLOW}[CRITICAL] {message}{NOCOLOR}",
849 Severity.CriticalNote: "{DARK_YELLOW} > {message}{NOCOLOR}",
850 Severity.Warning: "{YELLOW}[WARNING] {message}{NOCOLOR}",
851 Severity.WarningNote: "{DARK_YELLOW} > {message}{NOCOLOR}",
852 Severity.Info: "{WHITE}{message}{NOCOLOR}",
853 Severity.Normal: "{WHITE}{message}{NOCOLOR}",
854 Severity.DryRun: "{DARK_CYAN}[DRY] {message}{NOCOLOR}",
855 Severity.Verbose: "{GRAY}{message}{NOCOLOR}",
856 Severity.Debug: "{DARK_GRAY}{message}{NOCOLOR}"
857 } #: Message formatting rules.
859 _LOG_LEVEL_ROUTING__: Dict[Severity, Tuple[Callable[[str, str], int]]] #: Message routing rules.
860 _verbose: bool
861 _debug: bool
862 _silent: bool
863 _quiet: bool
864 _writeLevel: Severity
865 _writeToStdOut: bool
867 _lines: List[Line]
868 _baseIndent: int
870 _errorCount: int
871 _criticalWarningCount: int
872 _warningCount: int
874 HeadLine: ClassVar[str]
876 def __init__(self, mode: Mode = Mode.AllLinearToStdOut) -> None:
877 """
878 Initializer of a line-based terminal interface.
880 :param mode: Defines what output (normal, error, data) to write where. Default: a linear flow all to *STDOUT*.
881 """
882 TerminalBaseApplication.__init__(self)
883 # ILineTerminal.__init__(self, self)
885 self._LOG_LEVEL_ROUTING__ = {}
886 self.__InitializeLogLevelRouting(mode)
888 self._verbose = False
889 self._debug = False
890 self._silent = False
891 self._quiet = False
892 self._writeLevel = Severity.Normal
893 self._writeToStdOut = True
895 self._lines = []
896 self._baseIndent = 0
898 self._errorCount = 0
899 self._criticalWarningCount = 0
900 self._warningCount = 0
902 def __InitializeLogLevelRouting(self, mode: Mode = Mode.AllLinearToStdOut) -> None:
903 if mode is Mode.TextToStdOut_ErrorsToStdErr:
904 for severity in Severity:
905 if severity >= Severity.Silent and severity != Severity.Quiet:
906 self._LOG_LEVEL_ROUTING__[severity] = (self.WriteLineToStdErr,)
907 else:
908 self._LOG_LEVEL_ROUTING__[severity] = (self.WriteLineToStdOut,)
909 elif mode is Mode.AllLinearToStdOut:
910 for severity in Severity:
911 self._LOG_LEVEL_ROUTING__[severity] = (self.WriteLineToStdOut, )
912 elif mode is Mode.DataToStdOut_OtherToStdErr:
913 for severity in Severity:
914 self._LOG_LEVEL_ROUTING__[severity] = (self.WriteLineToStdErr, )
915 else: # pragma: no cover
916 ex = ExceptionBase(f"Unsupported mode '{mode}'.")
917 ex.add_note(f"Unsupported modes '{', '.join(m.name for m in Mode)}'.")
918 raise ex
920 def _PrintHeadline(self, width: int = 80) -> None:
921 """
922 Helper method to print the program headline.
924 :param width: Number of characters for horizontal lines.
926 .. admonition:: Generated output
928 .. code-block::
930 =========================
931 centered headline
932 =========================
933 """
934 if width == 0: 934 ↛ 935line 934 didn't jump to line 935 because the condition on line 934 was never true
935 width = self._width
937 self.WriteNormal(f"{{HEADLINE}}{'=' * width}".format(**TerminalApplication.Foreground))
938 self.WriteNormal(f"{{HEADLINE}}{{headline: ^{width}s}}".format(headline=self.HeadLine, **TerminalApplication.Foreground))
939 self.WriteNormal(f"{{HEADLINE}}{'=' * width}".format(**TerminalApplication.Foreground))
941 def _PrintHelp(self, command: Nullable[str] = None) -> None:
942 """
943 Helper function to print the command line parsers help page(s).
945 :param command: The subcommand to print the help page(s) for.
946 """
947 if command is None:
948 self.MainParser.print_help()
949 elif command == "help":
950 self.WriteWarning("This is a recursion ...")
951 else:
952 try:
953 self.SubParsers[command].print_help()
954 except KeyError:
955 self.WriteError(f"Command {command} is unknown.")
957 def _PrintVersion(
958 self,
959 dunderModule: ModuleType,
960 packageName: Nullable[str] = None,
961 versionCheckTimeout: int = 1
962 ) -> None:
963 """
964 Helper method to print the version information.
966 :param dunderModule: The Python module containing the dunder variables for author(s), email, copyright, version, ...
968 .. admonition:: Example usage
970 .. code-block:: Python
972 def _PrintVersion(self):
973 import myPackage.MyModule as DunderModule
975 super()._PrintVersion(
976 DunderModule,
977 "MyModule"
978 )
979 """
980 copyrights = getattr(dunderModule, "__copyright__", "{RED}Copyright not set!".format(RED=Foreground.RED)).split("\n", 1)
981 self.WriteNormal(f"Copyright: {copyrights[0]}")
982 for copyright in copyrights[1:]:
983 self.WriteNormal(f" {copyright}")
985 license = getattr(dunderModule, "__license__", "{RED}License not set!".format(RED=Foreground.RED))
986 self.WriteNormal(f"License: {license}")
988 authors = getattr(dunderModule, "__author__", "{RED}Unknown author!".format(RED=Foreground.RED)).split(", ")
989 self.WriteNormal(f"Authors: {authors[0]}")
990 for author in authors[1:]:
991 self.WriteNormal(f" {author}")
993 if (email := getattr(dunderModule, "__email__", None)) is not None:
994 self.WriteNormal(f"Email: {email}")
996 if (version := getattr(dunderModule, "__version__", None)) is None:
997 self.WriteNormal("Version: {RED}Version not set!".format(RED=Foreground.RED))
998 else:
999 currentVersion = PythonVersion.Parse(version)
1000 if packageName is None:
1001 update = ""
1002 elif (pypiVersion := self._GetLatestVersion(packageName, versionCheckTimeout)) is not None:
1003 latestVersion = PythonVersion.Parse(pypiVersion)
1004 update = f" (Update available: v{latestVersion})" if currentVersion < latestVersion else " (latest)"
1005 else:
1006 update = " (PyPI timeout)"
1007 self.WriteNormal(f"Version: v{version}{update}")
1009 if (projectURL := getattr(dunderModule, "__project_url__", None)) is not None:
1010 self.WriteNormal(f"Project: {projectURL}")
1012 if (documentationURL := getattr(dunderModule, "__documentation_url__", None)) is not None:
1013 self.WriteNormal(f"Documentation: {documentationURL}")
1015 if (issueTrackerURL := getattr(dunderModule, "__issue_tracker_url__", None)) is not None:
1016 self.WriteNormal(f"Issue tracker: {issueTrackerURL}")
1018 def _GetLatestVersion(self, packageName: str, timeout: int = 1) -> Nullable[str]:
1019 from json import loads
1020 from urllib.request import urlopen, Request
1022 request = Request(
1023 url=f"https://pypi.org/pypi/{packageName}/json",
1024 headers={'User-Agent': f'{packageName}-Version-Check'}
1025 )
1026 try:
1027 with urlopen(request, timeout=timeout) as response:
1028 data = loads(response.read().decode())
1029 return data["info"]["version"]
1030 except Exception:
1031 return None
1033 def Configure(
1034 self,
1035 *,
1036 verbose: bool = False,
1037 debug: bool = False,
1038 silent: bool = False,
1039 quiet: bool = False,
1040 writeToStdOut: bool = True
1041 ) -> None:
1042 self._verbose = True if debug else verbose
1043 self._debug = debug
1044 self._silent = silent
1045 self._quiet = quiet
1047 if quiet: 1047 ↛ 1049line 1047 didn't jump to line 1049 because the condition on line 1047 was always true
1048 self._writeLevel = Severity.Quiet
1049 elif silent:
1050 self._writeLevel = Severity.Silent
1051 elif debug:
1052 self._writeLevel = Severity.Debug
1053 elif verbose:
1054 self._writeLevel = Severity.Verbose
1055 else:
1056 self._writeLevel = Severity.Normal
1058 self._writeToStdOut = writeToStdOut
1060 @readonly
1061 def Verbose(self) -> bool:
1062 """Returns true, if verbose messages are enabled."""
1063 return self._verbose
1065 @readonly
1066 def Debug(self) -> bool:
1067 """Returns true, if debug messages are enabled."""
1068 return self._debug
1070 @readonly
1071 def Silent(self) -> bool:
1072 """Returns true, if silent mode is enabled."""
1073 return self._silent
1075 @readonly
1076 def Quiet(self) -> bool:
1077 """Returns true, if quiet mode is enabled."""
1078 return self._quiet
1080 @property
1081 def LogLevel(self) -> Severity:
1082 """Return the current minimal severity level for writing."""
1083 return self._writeLevel
1085 @LogLevel.setter
1086 def LogLevel(self, value: Severity) -> None:
1087 """Set the minimal severity level for writing."""
1088 self._writeLevel = value
1090 @property
1091 def BaseIndent(self) -> int:
1092 return self._baseIndent
1094 @BaseIndent.setter
1095 def BaseIndent(self, value: int) -> None:
1096 self._baseIndent = value
1098 @readonly
1099 def WarningCount(self) -> int:
1100 """
1101 Read-only property to access the number of counted warnings.
1103 :return: Number of warnings.
1104 """
1105 return self._warningCount
1107 @readonly
1108 def CriticalWarningCount(self) -> int:
1109 """
1110 Read-only property to access the number of counted critical warnings.
1112 :return: Number of critical warnings.
1113 """
1114 return self._criticalWarningCount
1116 @readonly
1117 def ErrorCount(self) -> int:
1118 """
1119 Read-only property to access the number of counted errors.
1121 :return: Number of errors.
1122 """
1123 return self._errorCount
1125 @readonly
1126 def Lines(self) -> List[Line]:
1127 """
1128 Read-only property to access the list of printed lines (messages).
1130 :returns: List of lines.
1131 """
1132 return self._lines
1134 def ExitOnPreviousErrors(self) -> None:
1135 """
1136 Exit application if errors have been printed.
1137 """
1138 if self._errorCount > 0:
1139 self.WriteFatal("Too many errors in previous steps.")
1141 def ExitOnPreviousCriticalWarnings(
1142 self,
1143 includeErrors: bool = True
1144 ) -> None:
1145 """
1146 Exit application if error or critical warnings have been printed.
1148 :param includeErrors: Include critical warning counts.
1149 """
1150 if includeErrors and (self._errorCount > 0): 1150 ↛ 1151line 1150 didn't jump to line 1151 because the condition on line 1150 was never true
1151 if self._criticalWarningCount > 0:
1152 self.WriteFatal("Too many errors and critical warnings in previous steps.")
1153 else:
1154 self.WriteFatal("Too many errors in previous steps.")
1155 elif self._criticalWarningCount > 0:
1156 self.WriteFatal("Too many critical warnings in previous steps.")
1158 def ExitOnPreviousWarnings(
1159 self,
1160 includeCriticalWarnings: bool = True,
1161 includeErrors: bool = True
1162 ) -> None:
1163 """
1164 Exit application if error or (critical) warnings have been printed.
1166 :param includeCriticalWarnings: Include critical warning counts.
1167 :param includeErrors: Include error counts.
1168 """
1169 if includeErrors and (self._errorCount > 0): 1169 ↛ 1170line 1169 didn't jump to line 1170 because the condition on line 1169 was never true
1170 if includeCriticalWarnings and (self._criticalWarningCount > 0):
1171 if self._warningCount > 0:
1172 self.WriteFatal("Too many errors and (critical) warnings in previous steps.")
1173 else:
1174 self.WriteFatal("Too many errors and critical warnings in previous steps.")
1175 elif self._warningCount > 0:
1176 self.WriteFatal("Too many warnings in previous steps.")
1177 else:
1178 self.WriteFatal("Too many errors in previous steps.")
1179 elif includeCriticalWarnings and (self._criticalWarningCount > 0): 1179 ↛ 1180line 1179 didn't jump to line 1180 because the condition on line 1179 was never true
1180 if self._warningCount > 0:
1181 self.WriteFatal("Too many (critical) warnings in previous steps.")
1182 else:
1183 self.WriteFatal("Too many critical warnings in previous steps.")
1184 elif self._warningCount > 0:
1185 self.WriteFatal("Too many warnings in previous steps.")
1187 def WriteLine(self, line: Line) -> bool:
1188 """
1189 Print a formatted line to the underlying terminal/console offered by the operating system.
1191 :param line: Line object to indent, format and print.
1192 :returns: True, if line was actually written.
1193 """
1194 if line.Severity < self._writeLevel:
1195 return False
1197 self._lines.append(line)
1198 for method in self._LOG_LEVEL_ROUTING__[line.Severity]:
1199 method(self._LOG_MESSAGE_FORMAT__[line.Severity].format(message=line.Message, **self.Foreground), end="\n" if line.AppendLinebreak else "")
1201 return True
1203 def TryWriteLine(self, line) -> bool:
1204 """
1205 Check if a line object of a certain severity would be written.
1207 :param line: Line object to check.
1208 :returns: True, if line would be written.
1209 """
1210 return line.Severity >= self._writeLevel
1212 def WriteFatal(
1213 self,
1214 message: str,
1215 *,
1216 indent: int = 0,
1217 appendLinebreak: bool = True,
1218 exitCode: int = 0,
1219 immediateExit: bool = True
1220 ) -> bool:
1221 """
1222 Write a fatal message and exit.
1224 Depending on internal settings and rules, a message might be skipped.
1226 :param message: Message to write.
1227 :param indent: Optional, indentation level of the message.
1228 :param appendLinebreak: Optional, append a linebreak after the message. Default: ``True``
1229 :param exitCode: Optional, exit application with this exit code. Default: ``0`` |br|
1230 If ``0``, use :attr:`FATAL_EXIT_CODE` as exit code.
1231 :param immediateExit: Optional, exit application immediately. Default: ``True``
1232 :returns: True, if message was actually written.
1233 """
1234 ret = self.WriteLine(Line(message, Severity.Fatal, indent=self._baseIndent + indent, appendLinebreak=appendLinebreak))
1235 if immediateExit:
1236 self.FatalExit(exitCode)
1237 return ret
1239 def WriteError(
1240 self,
1241 message: str,
1242 *,
1243 indent: int = 0,
1244 appendLinebreak: bool = True
1245 ) -> bool:
1246 """
1247 Write an error message.
1249 Depending on internal settings and rules, a message might be skipped.
1251 :param message: Message to write.
1252 :param indent: Optional, indentation level of the message.
1253 :param appendLinebreak: Optional, append a linebreak after the message. Default: ``True``
1254 :returns: True, if message was actually written.
1255 """
1256 self._errorCount += 1
1257 return self.WriteLine(Line(message, Severity.Error, indent=self._baseIndent + indent, appendLinebreak=appendLinebreak))
1259 def WriteQuiet(
1260 self,
1261 message: str,
1262 *,
1263 indent: int = 0,
1264 appendLinebreak: bool = True
1265 ) -> bool:
1266 """
1267 Write an always visible message.
1269 This message is even visible in quiet mode.
1271 Depending on internal settings and rules, a message might be skipped.
1273 :param message: Message to write.
1274 :param indent: Optional, indentation level of the message.
1275 :param appendLinebreak: Optional, append a linebreak after the message. Default: ``True``
1276 :returns: True, if message was actually written.
1277 """
1278 return self.WriteLine(Line(message, Severity.Quiet, indent=self._baseIndent + indent, appendLinebreak=appendLinebreak))
1280 def WriteCritical(
1281 self,
1282 message: str,
1283 *,
1284 indent: int = 0,
1285 appendLinebreak: bool = True
1286 ) -> bool:
1287 """
1288 Write a critical message.
1290 Depending on internal settings and rules, a message might be skipped.
1292 :param message: Message to write.
1293 :param indent: Optional, indentation level of the message.
1294 :param appendLinebreak: Optional, append a linebreak after the message. Default: ``True``
1295 :returns: True, if message was actually written.
1296 """
1297 self._criticalWarningCount += 1
1298 return self.WriteLine(Line(message, Severity.Critical, indent=self._baseIndent + indent, appendLinebreak=appendLinebreak))
1300 def WriteCriticalNote(
1301 self,
1302 message: str,
1303 *,
1304 indent: int = 0,
1305 appendLinebreak: bool = True
1306 ) -> bool:
1307 """
1308 Write a critical note.
1310 Depending on internal settings and rules, a note might be skipped.
1312 :param message: Message to write.
1313 :param indent: Optional, indentation level of the note.
1314 :param appendLinebreak: Optional, append a linebreak after the note. Default: ``True``
1315 :returns: True, if note was actually written.
1316 """
1317 return self.WriteLine(Line(message, Severity.CriticalNote, indent=self._baseIndent + indent, appendLinebreak=appendLinebreak))
1319 def WriteWarning(
1320 self,
1321 message: str,
1322 *,
1323 indent: int = 0,
1324 appendLinebreak: bool = True
1325 ) -> bool:
1326 """
1327 Write a warning message.
1329 Depending on internal settings and rules, a message might be skipped.
1331 :param message: Message to write.
1332 :param indent: Optional, indentation level of the message.
1333 :param appendLinebreak: Optional, append a linebreak after the message. Default: ``True``
1334 :returns: True, if message was actually written.
1335 """
1336 self._warningCount += 1
1337 return self.WriteLine(Line(message, Severity.Warning, indent=self._baseIndent + indent, appendLinebreak=appendLinebreak))
1339 def WriteWarningNote(
1340 self,
1341 message: str,
1342 *,
1343 indent: int = 0,
1344 appendLinebreak: bool = True
1345 ) -> bool:
1346 """
1347 Write a warning note.
1349 Depending on internal settings and rules, a note might be skipped.
1351 :param message: Message to write.
1352 :param indent: Optional, indentation level of the note.
1353 :param appendLinebreak: Optional, append a linebreak after the note. Default: ``True``
1354 :returns: True, if note was actually written.
1355 """
1356 return self.WriteLine(Line(message, Severity.WarningNote, indent=self._baseIndent + indent, appendLinebreak=appendLinebreak))
1358 def WriteInfo(
1359 self,
1360 message: str,
1361 *,
1362 indent: int = 0,
1363 appendLinebreak: bool = True
1364 ) -> bool:
1365 """
1366 Write an info message.
1368 Depending on internal settings and rules, a message might be skipped.
1370 :param message: Message to write.
1371 :param indent: Optional, indentation level of the message.
1372 :param appendLinebreak: Optional, append a linebreak after the message. Default: ``True``
1373 :returns: True, if message was actually written.
1374 """
1375 return self.WriteLine(Line(message, Severity.Info, indent=self._baseIndent + indent, appendLinebreak=appendLinebreak))
1377 def WriteNormal(
1378 self,
1379 message: str,
1380 *,
1381 indent: int = 0,
1382 appendLinebreak: bool = True
1383 ) -> bool:
1384 """
1385 Write a normal message.
1387 Depending on internal settings and rules, a message might be skipped.
1389 :param message: Message to write.
1390 :param indent: Optional, indentation level of the message.
1391 :param appendLinebreak: Optional, append a linebreak after the message. Default: ``True``
1392 :returns: True, if message was actually written.
1393 """
1394 return self.WriteLine(Line(message, Severity.Normal, indent=self._baseIndent + indent, appendLinebreak=appendLinebreak))
1396 def WriteVerbose(
1397 self,
1398 message: str,
1399 *,
1400 indent: int = 0,
1401 appendLinebreak: bool = True
1402 ) -> bool:
1403 """
1404 Write a verbose message.
1406 Depending on internal settings and rules, a message might be skipped.
1408 :param message: Message to write.
1409 :param indent: Optional, indentation level of the message.
1410 :param appendLinebreak: Optional, append a linebreak after the message. Default: ``True``
1411 :returns: True, if message was actually written.
1412 """
1413 return self.WriteLine(Line(message, Severity.Verbose, indent=self._baseIndent + indent, appendLinebreak=appendLinebreak))
1415 def WriteDebug(
1416 self,
1417 message: str,
1418 *,
1419 indent: int = 0,
1420 appendLinebreak: bool = True
1421 ) -> bool:
1422 """
1423 Write a debug message.
1425 Depending on internal settings and rules, a message might be skipped.
1427 :param message: Message to write.
1428 :param indent: Optional, indentation level of the message.
1429 :param appendLinebreak: Optional, append a linebreak after the message. Default: ``True``
1430 :returns: True, if message was actually written.
1431 """
1432 return self.WriteLine(Line(message, Severity.Debug, indent=self._baseIndent + indent, appendLinebreak=appendLinebreak))
1434 def WriteDryRun(
1435 self,
1436 message: str,
1437 *,
1438 indent: int = 0,
1439 appendLinebreak: bool = True
1440 ) -> bool:
1441 """
1442 Write a dry-run message message.
1444 Depending on internal settings and rules, a message might be skipped.
1446 :param message: Message to write.
1447 :param indent: Optional, indentation level of the message.
1448 :param appendLinebreak: Optional, append a linebreak after the message. Default: ``True``
1449 :returns: True, if message was actually written.
1450 """
1451 return self.WriteLine(Line(message, Severity.DryRun, indent=self._baseIndent + indent, appendLinebreak=appendLinebreak))