Source code for discord._logging

from datetime import datetime, UTC
from enum import StrEnum
from os import environ, getcwd, getenv, get_terminal_size
from sys import settrace
from traceback import FrameSummary, print_exc, TracebackException
from typing import Optional, Self


class LogColor(StrEnum):
  BG_CYAN: str     = "\033[46m"
  BG_ORANGE: str   = "\033[48;5;208m"
  BG_RED: str      = "\033[101m"
  BG_YELLOW: str   = "\033[103m"
  CYAN: str        = "\033[96m"
  ORANGE: str      = "\033[38;5;208m"
  RED: str         = "\033[91m"
  RESET: str       = "\033[0m"
  YELLOW: str      = "\033[93m"


class LogType(StrEnum):
  DEBUG: str = "DEBUG"
  ERROR: str = "ERROR"
  INFO: str  = " INFO"
  WARN: str  = " WARN"


[docs] class Logger: """Singleton logger instance""" __allow_logging: bool = getenv("demoutrei.discord::with_logger") is not None __debug_flag: bool = bool(getenv("demoutrei.discord::debug_enabled", False)) def __enter__(self) -> Self: self.__in_context_manager: bool = True settrace(None) return self def __exit__(self, exception_type: Optional[type[BaseException]], exception: Optional[BaseException], traceback: Optional[TracebackException]) -> bool: if exception is not None and exception_type is not KeyboardInterrupt: self.error(exception) return True self.log() def __context_manager_check(self, frame, event, argument): if not self.__in_context_manager and event == "return": self.log() settrace(None) return self.__context_manager_check @property def character_limit(self) -> int: """:meta private:""" return max(self.columns - (len(str(self.timestamp)) + 11), 75) @property def columns(self) -> int: """:meta private:""" return get_terminal_size().columns
[docs] @classmethod def debug(cls, *messages: str) -> Self: """:param messages: Messages to log as DEBUG level""" if not messages: raise ValueError(f"messages: Must pass at least one message") messages: list[str] = list(messages) for index, message in enumerate(messages): if not isinstance(message, str): raise TypeError(f"messages[{index}]: Must be an instance of {str}; not {message.__class__}") messages[index]: str = message.strip() instance: Self = super().__new__(cls) instance.__in_context_manager: bool = False instance.__messages: list[str] = messages instance.__type: LogType = LogType.DEBUG settrace(instance._Logger__context_manager_check) return instance
[docs] @classmethod def error(cls, exception: BaseException, /) -> Self: """:param exception: Exception to log as ERROR level""" if not isinstance(exception, BaseException): raise TypeError(f"exception: Must be an instance of {BaseException}; not {exception.__class__}") instance: Self = super().__new__(cls) instance.__exception: BaseException = exception instance.__in_context_manager: bool = False instance.__type: LogType = LogType.ERROR settrace(instance._Logger__context_manager_check) return instance
def get_prefix(self, *, with_level: bool = True, with_timestamp: bool = True) -> str: """:meta private:""" if not isinstance(self.log_type, LogType): raise TypeError(f"Logger.log_type: Must be an instance of {LogType}; not {self.log_type.__class__}") if not isinstance(with_level, bool): raise TypeError(f"with_level: Must be an instance of {bool}; not {with_level.__class__}") if not isinstance(with_timestamp, bool): raise TypeError(f"with_timestamp: Must be an instance of {bool}; not {with_timestamp.__class__}") level: str = f"[{f"{self.log_type}":^5}]" if not with_level: level: str = " " * len(level) timestamp: str = self.timestamp if not with_timestamp: timestamp: str = " " * len(timestamp) match self.log_type: case LogType.DEBUG: color, bg = LogColor.YELLOW, LogColor.BG_YELLOW case LogType.ERROR: color, bg = LogColor.RED, LogColor.BG_RED case LogType.INFO: color, bg = LogColor.CYAN, LogColor.BG_CYAN case LogType.WARN: color, bg = LogColor.ORANGE, LogColor.BG_ORANGE return f"{color}{timestamp} {bg} {LogColor.RESET}{color} {level}{LogColor.RESET}"
[docs] @classmethod def info(cls, *messages: str) -> Self: """:param messages: Messages to log as INFO level""" if not messages: raise ValueError(f"messages: Must pass at least one message") messages: list[str] = list(messages) for index, message in enumerate(messages): if not isinstance(message, str): raise TypeError(f"messages[{index}]: Must be an instance of {str}; not {message.__class__}") messages[index]: str = message.strip() instance: Self = super().__new__(cls) instance.__in_context_manager: bool = False instance.__messages: list[str] = messages instance.__type: LogType = LogType.INFO settrace(instance._Logger__context_manager_check) return instance
@property def lines(self) -> int: """:meta private:""" return get_terminal_size().lines def log(self) -> None: """:meta private:""" print(f"{self.get_prefix()} {f"\n{self.get_prefix(with_level = False, with_timestamp = False)} ".join([message for message in self.messages])}") @property def log_type(self) -> LogType: """:meta private:""" return self.__type @property def messages(self) -> list[str]: """:meta private:""" if self.log_type is LogType.ERROR: traceback: TracebackException = TracebackException.from_exception(self.__exception, capture_locals = True, max_group_depth = 100, max_group_width = 100) frame: FrameSummary = None for stack in traceback.stack[::-1]: if stack.filename.startswith(getcwd()) and not (stack.filename.startswith(f"{getcwd()}\\\\venv") or stack.filename.startswith(f"{getcwd()}\\venv")): frame: FrameSummary = stack break if not frame: frame: FrameSummary = traceback.stack[-1] message: str = f"{frame.filename.replace(f"{getcwd()}\\".replace("\\\\", ""), "")}:{frame.lineno}\n" \ f"{self.split(frame.line, indent = False, indent_space = 2)}\n" \ f"{self.__exception.__class__.__name__}: {traceback}" message: str = message.replace("\n", f"\n{self.get_prefix(with_level = False, with_timestamp = False)} ") return [message] return [self.split(message) for message in self.__messages] def split(self, message: str, /, *, indent: bool = True, indent_space: int = 0) -> str: """:meta private:""" if not isinstance(message, str): raise TypeError(f"message: Must be an instance of {str}; not {message.__class__}") if not isinstance(indent, bool): raise TypeError(f"indent: Must be an instance of {bool}; not {indent.__class__}") if not isinstance(indent_space, int): raise TypeError(f"indent_space: Must be an instance of {int}; not {indent_space.__class__}") if indent_space < 0: raise ValueError(f"indent_space: Must be greater than or equal to 0") if not message.strip(): return message sections: list[str] = list() for i in range((len(message) // self.character_limit) + 1): index: int = (self.character_limit - indent_space) * i position: int = index + (self.character_limit - indent_space) section: str = f"{" " * indent_space}{message[index:position]}" sections.append(section) return f"\n{f"{self.get_prefix(with_level = False, with_timestamp = False)}" if indent else str()}".join(sections) @property def timestamp(self) -> str: """:meta private:""" return datetime.now().strftime(r"%Y-%m-%d %H:%M:%S")
[docs] @classmethod def warn(cls, *messages: str) -> Self: """:param messages: Messages to log as WARN level""" if not messages: raise ValueError(f"messages: Must pass at least one message") messages: list[str] = list(messages) for index, message in enumerate(messages): if not isinstance(message, str): raise TypeError(f"messages[{index}]: Must be an instance of {str}; not {message.__class__}") messages[index]: str = message.strip() instance: Self = super().__new__(cls) instance.__in_context_manager: bool = False instance.__messages: list[str] = messages instance.__type: LogType = LogType.WARN settrace(instance._Logger__context_manager_check) return instance