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
« 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
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: 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,
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": "",
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 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
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__: 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.
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 access 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 """
763 Read-only property to access the local terminal instance (:attr:`_terminal`).
765 :returns: The terminal instance, or ``None`` if no terminal is attached.
766 """
767 return self._terminal
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
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
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
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
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
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
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
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
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
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
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
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
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.
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
871 _lines: List[Line]
872 _baseIndent: int
874 _errorCount: int
875 _criticalWarningCount: int
876 _warningCount: int
878 HeadLine: ClassVar[str]
880 def __init__(self, mode: Mode = Mode.AllLinearToStdOut) -> None:
881 """
882 Initializer of a line-based terminal interface.
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)
889 self._LOG_LEVEL_ROUTING__ = {}
890 self.__InitializeLogLevelRouting(mode)
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
899 self._lines = []
900 self._baseIndent = 0
902 self._errorCount = 0
903 self._criticalWarningCount = 0
904 self._warningCount = 0
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
924 def _PrintHeadline(self, width: int = 80) -> None:
925 """
926 Helper method to print the program headline.
928 :param width: Number of characters for horizontal lines.
930 .. admonition:: Generated output
932 .. code-block::
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
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))
945 def _PrintHelp(self, command: Nullable[str] = None) -> None:
946 """
947 Helper function to print the command line parsers help page(s).
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.")
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.
970 :param dunderModule: The Python module containing the dunder variables for author(s), email, copyright, version, ...
972 .. admonition:: Example usage
974 .. code-block:: Python
976 def _PrintVersion(self):
977 import myPackage.MyModule as DunderModule
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}")
989 license = getattr(dunderModule, "__license__", "{RED}License not set!".format(RED=Foreground.RED))
990 self.WriteNormal(f"License: {license}")
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}")
997 if (email := getattr(dunderModule, "__email__", None)) is not None:
998 self.WriteNormal(f"Email: {email}")
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}")
1013 if (projectURL := getattr(dunderModule, "__project_url__", None)) is not None:
1014 self.WriteNormal(f"Project: {projectURL}")
1016 if (documentationURL := getattr(dunderModule, "__documentation_url__", None)) is not None:
1017 self.WriteNormal(f"Documentation: {documentationURL}")
1019 if (issueTrackerURL := getattr(dunderModule, "__issue_tracker_url__", None)) is not None:
1020 self.WriteNormal(f"Issue tracker: {issueTrackerURL}")
1022 def _GetLatestVersion(self, packageName: str, timeout: int = 1) -> Nullable[str]:
1023 from json import loads
1024 from urllib.request import urlopen, Request
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
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
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
1062 self._writeToStdOut = writeToStdOut
1064 @readonly
1065 def Verbose(self) -> bool:
1066 """
1067 Check if verbose messages are enabled.
1069 :returns: ``True``, if verbose messages are written.
1070 """
1071 return self._verbose
1073 @readonly
1074 def Debug(self) -> bool:
1075 """
1076 Check if debug messages are enabled.
1078 :returns: ``True``, if debug messages are written.
1079 """
1080 return self._debug
1082 @readonly
1083 def Silent(self) -> bool:
1084 """
1085 Check if silent mode is enabled.
1087 :returns: ``True``, if silent mode is enabled.
1088 """
1089 return self._silent
1091 @readonly
1092 def Quiet(self) -> bool:
1093 """
1094 Check if quiet mode is enabled.
1096 :returns: ``True``, if quiet mode is enabled.
1097 """
1098 return self._quiet
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`).
1105 :returns: The current minimal severity level.
1106 """
1107 return self._writeLevel
1109 @LogLevel.setter
1110 def LogLevel(self, value: Severity) -> None:
1111 """Set the minimal severity level for writing."""
1112 self._writeLevel = value
1114 @property
1115 def BaseIndent(self) -> int:
1116 """
1117 Read-only property to access the base indentation level of written messages (:attr:`_baseIndent`).
1119 :returns: Base indentation level.
1120 """
1121 return self._baseIndent
1123 @BaseIndent.setter
1124 def BaseIndent(self, value: int) -> None:
1125 self._baseIndent = value
1127 @readonly
1128 def WarningCount(self) -> int:
1129 """
1130 Read-only property to access the number of counted warnings.
1132 :returns: Number of warnings.
1133 """
1134 return self._warningCount
1136 @readonly
1137 def CriticalWarningCount(self) -> int:
1138 """
1139 Read-only property to access the number of counted critical warnings.
1141 :returns: Number of critical warnings.
1142 """
1143 return self._criticalWarningCount
1145 @readonly
1146 def ErrorCount(self) -> int:
1147 """
1148 Read-only property to access the number of counted errors.
1150 :returns: Number of errors.
1151 """
1152 return self._errorCount
1154 @readonly
1155 def Lines(self) -> List[Line]:
1156 """
1157 Read-only property to access the list of printed lines (messages).
1159 :returns: List of lines.
1160 """
1161 return self._lines
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.")
1170 def ExitOnPreviousCriticalWarnings(
1171 self,
1172 includeErrors: bool = True
1173 ) -> None:
1174 """
1175 Exit application if error or critical warnings have been printed.
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.")
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.
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.")
1216 def WriteLine(self, line: Line) -> bool:
1217 """
1218 Print a formatted line to the underlying terminal/console offered by the operating system.
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
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 "")
1230 return True
1232 def TryWriteLine(self, line) -> bool:
1233 """
1234 Check if a line object of a certain severity would be written.
1236 :param line: Line object to check.
1237 :returns: True, if line would be written.
1238 """
1239 return line.Severity >= self._writeLevel
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.
1253 Depending on internal settings and rules, a message might be skipped.
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
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.
1278 Depending on internal settings and rules, a message might be skipped.
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))
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.
1298 This message is even visible in quiet mode.
1300 Depending on internal settings and rules, a message might be skipped.
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))
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.
1319 Depending on internal settings and rules, a message might be skipped.
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))
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.
1339 Depending on internal settings and rules, a note might be skipped.
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))
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.
1358 Depending on internal settings and rules, a message might be skipped.
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))
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.
1378 Depending on internal settings and rules, a note might be skipped.
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))
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.
1397 Depending on internal settings and rules, a message might be skipped.
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))
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.
1416 Depending on internal settings and rules, a message might be skipped.
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))
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.
1435 Depending on internal settings and rules, a message might be skipped.
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))
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.
1454 Depending on internal settings and rules, a message might be skipped.
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))
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.
1473 Depending on internal settings and rules, a message might be skipped.
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))