Source code for tbot.log

"""
TBot logging facilities
-----------------------
"""
import sys
import enum
import json
import time
import typing
import pathlib


[docs]def has_unicode(with_unicode: str, without_unicode: str) -> str: """ Check whether the terminal supports unicode, if it does, returns the ``with_unicode`` string, if it doesn't, returns the ``without_unicode`` string. :param str with_unicode: String to be returned, if the terminal does support unicode :param str without_unicode: String to be returned, if the terminal does not support unicode :rtype: str """ if sys.stdout.encoding == "UTF-8": return with_unicode return without_unicode
[docs]def has_color(seq: str) -> str: """ Check color support and return the escape sequence for this color code if enabled. :param str seq: The color code, eg ``"1;31"`` :rtype: str """ if True: return f"\x1B[{seq}m" return ""
[docs]class Verbosity(enum.IntEnum): """ Logger verbosity level """ ALL = -1 ERROR = 0 WARNING = 1 INFO = 2 DEBUG = 3 VERBOSE = 4 VERY_VERBOSE = 5 OVER_VERBOSE = 6 NEVER = 1000000 def __str__(self) -> str: return super(Verbosity, self).__str__().split(".")[-1]
LOGFILE = None LOGLIST = list() LOGVERBOSITY = Verbosity.WARNING LOGNESTLAYER = 0 LOG_DO_DOC = True
[docs]def check_log() -> None: """ Check whether the log has been initialized and throw an exception otherwise. """ global LOGFILE # pylint: disable=global-statement if LOGFILE is None: raise Exception("Logfile was not initialized!")
[docs]class LogStdoutHandler: """ Handler for writing to stdout and dealing with a logevent after it's creation. You should not need to instanciate this object yourself, use :func:`tbot.log.event`. :ivar dct: The log event's dictionary, you can add additional things :ivar key: Key in the dictionary that output given to `.print()` should be appended to. :ivar prefix: A custom prefix that will be added in front of continuation lines. """ def __init__( self, dct: typing.Dict[str, typing.Any], verbosity: Verbosity, custom_dash: typing.Optional[str], ) -> None: global LOGVERBOSITY # pylint: disable=global-statement global LOGNESTLAYER # pylint: disable=global-statement self.do_output = verbosity <= LOGVERBOSITY self.is_continuation = False self.layer = LOGNESTLAYER self.custom_dash = custom_dash self.dct = dct self.key: typing.Optional[str] = None self.prefix: typing.Optional[str] = None
[docs] def reset_verbosity(self, new_verbosity: Verbosity) -> None: """ Change the verbosity of this log event :param Verbosity new_verbosity: The new verbosity """ global LOGVERBOSITY # pylint: disable=global-statement self.do_output = new_verbosity <= LOGVERBOSITY
[docs] def print(self, msg: str) -> None: """ Print some text to stdout provided the verbosity is high enough. Also, add the text to the dict in case a `key` was set. :param str msg: The text """ # Add to log event if self.key is not None: if msg == "": self.dct[self.key] += "\n" else: self.dct[self.key] += msg + ("" if msg[-1] == "\n" else "\n") if not self.do_output: return lines = msg.split("\n") for line in lines: msg_prefix = has_color("0") prfx = has_unicode("│ ", "| ") for _ in range(0, self.layer): msg_prefix += prfx if not self.is_continuation: if self.custom_dash is not None: msg_prefix += self.custom_dash else: msg_prefix += has_unicode("├─", "+-") self.is_continuation = True else: msg_prefix += has_unicode("│ ", "| ") if self.prefix is not None: msg_prefix += self.prefix print(msg_prefix + line + has_color("0"))
[docs]def event( ty: typing.List[str], *, msg: typing.Optional[str] = None, verbosity: Verbosity = Verbosity.INFO, dct: typing.Optional[typing.Dict[str, typing.Any]] = None, custom_dash: typing.Optional[str] = None, fake: bool = False, ) -> LogStdoutHandler: """ Create a new log event :param list[str] ty: The type of this log event :param str msg: An optional message intended for being printed on the screen :param Verbosity verbosity: Optional verbosity of this event (defaults to ``INFO``) :param dict dct: A dictionary of payload for this logevent. Can't contain keys named ``"type"``, ``"time"``, ``"message"``, or ``"verbosity"``. :param str custom_dash: Different prefix for the message when printed onscreen :param bool fake: Whether this is just a fake event that won't be logged :returns: A handler for the created log event :rtype: LogStdoutHandler """ global LOGLIST # pylint: disable=global-statement check_log() dct = dct or dict() forbidden = ["type", "time", "message", "verbosity"] for itm in forbidden: if itm in dct: raise Exception(f"A log event can not contain items named '{itm}'!") dct["type"] = ty dct["time"] = time.ctime() dct["verbosity"] = str(verbosity) stdout_handler = LogStdoutHandler(dct, verbosity, custom_dash) if msg is not None: dct["message"] = msg stdout_handler.print(msg) if not fake: LOGLIST.append(dct) return stdout_handler
[docs]def message(msg: str, verbosity: Verbosity = Verbosity.INFO) -> LogStdoutHandler: """ Print a message :param str msg: The message :param Verbosity verbosity: Optional verbosity for this message :returns: A handler for the created log event :rtype: LogStdoutHandler """ return event( ty=["msg", str(verbosity)], msg=msg, verbosity=verbosity, dct={"text": msg} )
[docs]def warning(msg: str) -> LogStdoutHandler: """ Print a warning message :param str msg: The message :returns: A handler for the created log event :rtype: LogStdoutHandler """ return message( has_color("33;1") + "Warning" + has_color("0") + ": " + msg, Verbosity.WARNING )
[docs]def error(msg: str) -> LogStdoutHandler: """ Print an error message :param str msg: The message :returns: A handler for the created log event :rtype: LogStdoutHandler """ return message( has_color("31;1") + "Error" + has_color("0") + ": " + msg, Verbosity.ERROR )
[docs]def doc(text: str) -> LogStdoutHandler: """ Add a log event that contains text for the documentation generator. ``text`` should be formatted in Markdown. :param str text: The documentation fragment :returns: A handler for the created log event :rtype: LogStdoutHandler """ return event( ty=["doc", "text"], verbosity=Verbosity.NEVER, dct={"text": text}, fake=not LOG_DO_DOC, )
[docs]def doc_appendix(title: str, text: str) -> LogStdoutHandler: """ Add a log event that contains an appendix for the documentation generator. ``text`` should be formatted in Markdown. :param str title: The appendix's title :param str text: The appendix's body :returns: A handler for the created log event :rtype: LogStdoutHandler """ return event( ty=["doc", "appendix"], verbosity=Verbosity.NEVER, dct={"title": title, "text": text}, fake=not LOG_DO_DOC, )
[docs]def debug(msg: str) -> LogStdoutHandler: """ Print a debug message :param str msg: The message :returns: A handler for the created log event :rtype: LogStdoutHandler """ return message(msg, Verbosity.DEBUG)
[docs]def oververbose(msg: str) -> LogStdoutHandler: """ Log a "oververbose" message, this is intended for very rarely needed debug output (just run TBot with this verbosity and see for yourself ...) Will not create a new log event. :param str msg: The message :returns: A handler for the message :rtype: LogStdoutHandler """ stdout_handler = LogStdoutHandler( dict(), Verbosity.OVER_VERBOSE, has_unicode("├>", "+>") ) stdout_handler.print(has_color("33") + msg) return stdout_handler
[docs]def set_layer(layer: int) -> None: """ Set the call graph depth. You should never need to call this yourself. """ global LOGNESTLAYER # pylint: disable=global-statement LOGNESTLAYER = layer
[docs]def init_log( filename: typing.Union[pathlib.Path, str], verbosity: Verbosity = Verbosity.INFO ) -> None: """ Initialize the logger :param filename: The file the log should be written to (in json format) :type filename: pathlib.Path or str :param Verbosity verbosity: The minimun verbosity for messages that are printed to stdout """ global LOGFILE # pylint: disable=global-statement global LOGVERBOSITY # pylint: disable=global-statement LOGFILE = pathlib.Path(filename) LOGVERBOSITY = verbosity
[docs]def flush_log() -> None: """ Write the log file """ global LOGLIST # pylint: disable=global-statement global LOGFILE # pylint: disable=global-statement check_log() if LOGFILE is not None: json.dump(LOGLIST, open(LOGFILE, "w"), indent=4)