# File: src/fitzzftw/patch/static.py
# Author: Fitzz TeXnik Welt
# Email: FitzzTeXnikWelt@t-online.de
# License: LGPLv2 or above
"""
static
===============================
Static shared resources for the ftw-patch project.
This module provides global access to color management via a pre-instantiated
singleton-like object (:const:`colors`).
"""
from pathlib import Path
from typing import Literal, TypeAlias, TypeVar, get_args
_T = TypeVar("_T")
ColorModes: TypeAlias = Literal["NORMAL", "PLAIN", "TEST"]
[docs]
class Color:
"""
Internal class for color and style management in terminal output.
Controls whether ANSI escape sequences, plain text, or semantic tags
(test mode) are returned. This class should not be instantiated directly;
use the provided 'colors' instance instead.
"""
_VALID_MODES:tuple[ColorModes] = get_args(ColorModes)
[docs]
def __init__(self):
"""
Initializes the color manager with default mode 'NORMAL'.
"""
self._mode: ColorModes = "NORMAL"
self._codes = {
"RED": ("\033[31m", "red>"),
"GREEN": ("\033[32m", "grn>"),
"YELLOW": ("\033[33m", "ylw>"),
"CYAN": ("\033[36m", "cyn>"),
"RESET": ("\033[0m", "<reset"),
"BOLD": ("\033[1m", "bold."),
"TERMINAL": ("", "trm>"),
}
@property
def mode(self) -> ColorModes:
"""
Get or set the current output mode.
:returns: The current mode ('NORMAL', 'PLAIN', or 'TEST').
:raises ValueError: If the assigned mode is not in allowed.
"""
return self._mode
@mode.setter
def mode(self, value: ColorModes) -> None:
"""
Set the current output mode.
:param value: The mode ('NORMAL', 'PLAIN', or 'TEST').
:raises ValueError: If the assigned mode is not allowed.
"""
normalized = value.upper()
if normalized not in self._VALID_MODES:
raise ValueError(f"Invalid mode '{value}'. Must be one of {self._VALID_MODES}")
self._mode = normalized
@property
def defined_colors(self):
return [c.lower() for c in self._codes if c not in ["RESET", "BOLD"]]
@property
def defined_keys(self)-> list[str]:
return [c.lower() for c in self._codes]
[docs]
def switch_to_testmode(self, enabled: bool = True):
"""
Convenience method to toggle between TEST and NORMAL mode.
:param enabled: If True, sets mode to 'TEST', otherwise to 'NORMAL'.
"""
self.mode = "TEST" if enabled else "NORMAL"
def _get_value(self, name: str) -> str:
"""
Returns the appropriate string representation for a style name
based on the current mode.
:param name: The name of the color or style.
:returns: ANSI code, test marker, or empty string.
"""
if self._mode == "PLAIN":
return ""
color, test = self._codes.get(name, ("",""))
return test if self._mode == "TEST" else color
@property
def RED(self) -> str:
"""
The ANSI sequence or marker for red.
:returns: Red style string.
"""
return self._get_value("RED")
@property
def GREEN(self) -> str:
"""
The ANSI sequence or marker for green.
:returns: Green style string.
"""
return self._get_value("GREEN")
@property
def YELLOW(self) -> str:
"""The ANSI sequence or marker for dark yellow."""
return self._get_value("YELLOW")
@property
def CYAN(self) -> str:
"""
The ANSI sequence or marker for cyan.
:returns: Cyan style string.
:rtype: str
"""
return self._get_value("CYAN")
@property
def RESET(self) -> str:
"""
The ANSI sequence or marker to reset styling.
:returns: Reset style string.
:rtype: str
"""
return self._get_value("RESET")
@property
def BOLD(self) -> str:
"""
The ANSI sequence or marker for bold text.
:returns: Bold style string.
"""
return self._get_value("BOLD")
@property
def TERMINAL(self)->str:
"""
The ANSI sequence or marker for terminal color.
:returns: Terminal style string.
:rtype: str
"""
return self._get_value("TERMINAL")
def __getitem__(self, key: str) -> str:
"""
Enables dictionary-like access for style names.
:param key: Style name (e.g., 'red', 'bold').
:returns: The corresponding ANSI code, marker, or empty string.
"""
return self._get_value(key.upper())
[docs]
def get(self, color:str, default:_T=None) -> _T|str:
"""
Get a color value by key with a fallback default.
:param key: The name of the color (e.g., 'red').
:param default: The value to return if the key is not found.
:returns: The color string or the default value.
"""
if self._mode == "PLAIN":
return ""
return self._get_value(color.upper())
ColorKey: TypeAlias = Literal["red", "green", "yellow", "cyan","terminal"]
TerminalKey: TypeAlias = Literal["bold", "reset" ,"red", "green", "yellow", "cyan", "terminal"]
colors:Color = Color()
"""Global instance for project-wide use."""
if __debug__: #pragma: no cover
def _check_colors() -> None:
actual_keys = set(Color().defined_colors)
literal_keys = set(get_args(ColorKey))
if actual_keys != literal_keys:
missing = actual_keys - literal_keys
extra = literal_keys - actual_keys
error_msg = ["Update ColorKey-Literal:"]
if missing:
error_msg.append(f" - Missing in Literal: {missing}")
if extra:
error_msg.append(f" - Extra in Literal (obsolete): {extra}")
print("\n".join(error_msg))
def _check_terminal() -> None:
actual_keys = set(Color().defined_keys)
literal_keys = set(get_args(TerminalKey))
if actual_keys != literal_keys:
missing = actual_keys - literal_keys
extra = literal_keys - actual_keys
error_msg = ["Update TerminalKey-Literal:"]
if missing:
error_msg.append(f" - Missing in Literal: {missing}")
if extra:
error_msg.append(f" - Extra in Literal (obsolete): {extra}")
print("\n".join(error_msg))
_check_colors()
_check_terminal()
if __name__ == "__main__": # pragma: no cover
from doctest import FAIL_FAST, testfile
be_verbose = False
be_verbose = True
option_flags = 0
option_flags = FAIL_FAST
test_sum = 0
test_failed = 0
# Pfad zu den dokumentierenden Tests
testfiles_dir = Path(__file__).parents[3] / "doc/source/devel"
test_file = testfiles_dir / "get_started_static.rst"
if test_file.exists():
print(f"--- Running Doctest for {test_file.name} ---")
doctestresult = testfile(
str(test_file),
module_relative=False,
verbose=be_verbose,
optionflags=option_flags,
)
test_failed += doctestresult.failed
test_sum += doctestresult.attempted
if test_failed == 0:
print(f"\nDocTests passed without errors, {test_sum} tests.")
else:
print(f"\nDocTests failed: {test_failed} tests.")
else:
print(f"⚠️ Warning: Test file {test_file.name} not found.")