from __future__ import annotations import logging import sys from html import escape from typing import Any, Callable class Logger: colors_map = { "red": 31, "green": 32, "yellow": 33, "blue": 34, "magenta": 35, "cyan": 36, "white": 37, "grey": 38, } html_colors_map = { "background": "rgb(40, 44, 52)", "grey": "rgb(132, 139, 149)", "cyan": "rgb(86, 182, 194)", "blue": "rgb(97, 175, 239)", "red": "rgb(224, 108, 117)", "magenta": "rgb(198, 120, 221)", "yellow": "rgb(229, 192, 123)", "white": "rgb(220, 223, 228)", "green": "rgb(108, 135, 94)", } colors_dict = { "error": colors_map["red"], "info": colors_map["green"], "info ": colors_map["green"], "debug": colors_map["grey"], "other": colors_map["grey"], } default_options: dict[str, Any] = { "debug": False, "verbose": False, "nocolor": False, "log": sys.stderr, "format": "text", } def __init__(self, opts: dict[str, Any] | None = None, name: str = "engine"): self.options = dict(Logger.default_options) if opts: self.options.update(opts) logger_name = f"web_header_analyzer.{name}.{id(self)}" self._logger = logging.getLogger(logger_name) self._logger.propagate = False self._configure_logger() def _configure_logger(self) -> None: self._logger.handlers.clear() handler = self._build_handler(self.options.get("log")) if handler is None: handler = logging.NullHandler() handler.setFormatter(logging.Formatter("%(message)s")) if hasattr(handler, "terminator"): handler.terminator = "" self._logger.addHandler(handler) level = logging.DEBUG if self.options.get("debug") else logging.INFO self._logger.setLevel(level) @staticmethod def _build_handler(target: Any) -> logging.Handler | None: if target is None or target == "none": return None if isinstance(target, str): return logging.FileHandler(target, encoding="utf-8") if hasattr(target, "write"): return logging.StreamHandler(target) return logging.StreamHandler(sys.stderr) @staticmethod def with_color(c: int, s: str) -> str: return f"__COLOR_{c}__|{s}|__END_COLOR__" @staticmethod def replaceColors(s: str, colorizingFunc: Callable[[int, str], str]) -> str: pos = 0 while pos < len(s): if s[pos:].startswith("__COLOR_"): pos += len("__COLOR_") pos1 = s[pos:].find("__|") if pos1 == -1: raise ValueError( "Output colors mismatch - could not find pos of end of " "color number!" ) c = int(s[pos : pos + pos1]) pos += pos1 + len("__|") pos2 = s[pos:].find("|__END_COLOR__") if pos2 == -1: raise ValueError( "Output colors mismatch - could not find end of color marker!" ) txt = s[pos : pos + pos2] pos += pos2 + len("|__END_COLOR__") patt = f"__COLOR_{c}__|{txt}|__END_COLOR__" colored = colorizingFunc(c, txt) if not colored: raise ValueError(f"Could not strip colors from phrase: ({patt})!") s = s.replace(patt, colored) pos = 0 continue pos += 1 return s @staticmethod def noColors(s: str) -> str: return Logger.replaceColors(s, lambda _c, txt: txt) @staticmethod def ansiColors(s: str) -> str: return Logger.replaceColors(s, lambda c, txt: f"\x1b[{c}m{txt}\x1b[0m") @staticmethod def htmlColors(s: str) -> str: def get_col(c: int, txt: str) -> str: text = escape(txt) for k, v in Logger.colors_map.items(): if v == c: return f'{text}' return text return Logger.replaceColors(s, get_col) def colored(self, txt: str, col: str) -> str: if self.options["nocolor"]: return txt return Logger.with_color(Logger.colors_map[col], txt) def _level_for_mode(self, mode: str) -> int: normalized = mode.strip().lower() if normalized == "debug": return logging.DEBUG if normalized == "error": return logging.ERROR return logging.INFO def _emit(self, txt: Any, mode: str, **kwargs: Any) -> None: if txt is None: return args = { "color": None, "noprefix": False, "newline": True, "nocolor": self.options["nocolor"], } args.update(kwargs) if not isinstance(txt, str): txt = str(txt) txt = txt.replace("\t", " " * 4) if args["nocolor"]: col = None elif args["color"]: col = args["color"] if isinstance(col, str): col = Logger.colors_map.get(col, Logger.colors_map["grey"]) else: col = Logger.colors_dict.get(mode, Logger.colors_map["grey"]) prefix = "" if mode: mode_tag = f"[{mode}] " if not args["noprefix"]: if args["nocolor"]: prefix = mode_tag.upper() else: prefix = Logger.with_color( Logger.colors_dict["other"], mode_tag.upper() ) if args["nocolor"] or col is None: to_write = prefix + txt else: to_write = prefix + Logger.with_color(col, txt) if args["newline"]: to_write += "\n" if args["nocolor"]: to_write = Logger.noColors(to_write) else: to_write = Logger.ansiColors(to_write) self._logger.log(self._level_for_mode(mode), to_write) @staticmethod def out(txt: Any, fd: Any, mode: str = "info", **kwargs: Any) -> None: temp = Logger({"log": fd, "nocolor": kwargs.get("nocolor", False)}) temp._emit(txt, mode, **kwargs) def info(self, txt: Any, forced: bool = False, **kwargs: Any) -> None: if forced or self.options["verbose"] or self.options["debug"]: self._emit(txt, "info", **kwargs) return log_target = self.options.get("log") if isinstance(log_target, str) and log_target != "none": self._emit(txt, "info", **kwargs) def text(self, txt: Any, **kwargs: Any) -> None: kwargs["noprefix"] = True self._emit(txt, "", **kwargs) def dbg(self, txt: Any, **kwargs: Any) -> None: if not self.options["debug"]: return if self.options.get("format") == "html": txt = f"" self._emit(txt, "debug", **kwargs) def err(self, txt: Any, **kwargs: Any) -> None: self._emit(txt, "error", **kwargs) def fatal(self, txt: Any, **kwargs: Any) -> None: self._emit(txt, "error", **kwargs) raise SystemExit(1) logger = Logger()