From 6ddb43d19465ff7c0a6615c20012a50f8cae2906 Mon Sep 17 00:00:00 2001 From: theonepath Date: Tue, 3 May 2022 18:13:18 +0100 Subject: [PATCH] Updated _logger.py Module given docstrings to extensively explain the purpose and function of defined methods within the class, and class structure itself. Method `genLogHeader` has been moved to `printfmt` module and renamed `gen_log_header`, along with appropriate private attributes now global constants in the module. --- _logger.py | 182 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 149 insertions(+), 33 deletions(-) diff --git a/_logger.py b/_logger.py index 97093ba..28cfaa5 100644 --- a/_logger.py +++ b/_logger.py @@ -1,15 +1,51 @@ -from collections import namedtuple import os import sys -import time from io import TextIOWrapper -from typing import TextIO, Union, TYPE_CHECKING +from typing import TextIO, Union +from collections import namedtuple from utils import common from utils.printfmt import * + class Logger: ... # class delcaration class Logger: # class redeclaration & initialisation + """A simple logging class to write messages directly to the console + or to a log file. + + Class contains a variety of methods to perform logging, all of which + invoke a private wrapper method over the built-in `print()` function, + with enhanced features built into the class methods. + + Make use of pseudolog methods (`Logger` pseudonyms) to quickly, and + effectively write a message to a log file. These pseudolog methods + modify the class state to remember the last message logged out to a + file, in addition to its formatting, which can then be written to + the console using the `Logger.withConsole()` method. + + Example of using pseudolog methods: + ```py + >>> import clog + >>> logger = clog.Logger() + >>> + >>> logger.debug("A debug message with stacktrace!") + >>> logger.error("Whoops! This should not be here.").withConsole() + \033[91mWhoops! This should not be here.\033[0m + >>> + >>> msg = "Checking if 1 + 1 = 2..." + >>> logger.debug(msg, end="\r").withConsole() + >>> if 1 + 1 != 2: + ... logger.error(msg + "failed.").withConsole() + ... else: + ... logger.debug(msg + "ok.").withConsole() + ``` + + Logger can output text to a console with colour, depending on its + associated log level given. Different standard `PIPE`s can also + be written to depending on the level of the log, or if a file + redirect descriptor has been given. Note, colour is omitted when + not writing to console on STDOUT or STDERR. + """ ## set the PIPE to STDOUT by default __stdpipe: TextIO = sys.stdout ## detect if there's a redirect @@ -17,10 +53,6 @@ class Logger: # class redeclaration & initialisation __IS_STDERR_REDIR: bool = os.isatty(sys.stderr.fileno()) ## the default log out file __DEFAULT_OUT_FILE: str = "dump.log" - ## log message constant format template - __LOG_TMPL = "[{DATE}] [{0}] {TYPE} " - ## string time format (ISO: 8601, long-form) - __TIMESTAMP_FMT = "%Y-%m-%dT%H:%M:%S%z" ## log info namedtuple for storing class states __LOG_INFO_TUPLE = namedtuple('LogInfo', ['isatty', 'lv', 'msg', 'sep', 'end']) @@ -31,13 +63,20 @@ class Logger: # class redeclaration & initialisation def __new__(cls, *, out_f: str = ...) -> Logger: + """Construct a new instance of the class and initialise it. + + Constructor method is used to establish the class as a + Singleton+Factory pattern. A new instance is returned from + the constructor, or if an existing instance is present, + return the object of that instance. + """ if cls.__instance__ is None: cls.__instance__ = super(Logger, cls).__new__(cls) # establish singleton instance cls.log = cls.__DEFAULT_OUT_FILE # initialise attribute to default ## handle if a custom file pathspec was given if out_f is not Ellipsis and isinstance(out_f, str): ## verify path and convert to real pathspec. - if common.isPathspec(out_f): + if common.is_path_spec(out_f): ## redefine the default log out attribute and create ## public attribute for the currect log location cls.log = cls.__DEFAULT_OUT_FILE = \ @@ -55,7 +94,16 @@ class Logger: # class redeclaration & initialisation def debug(cls, *value: object, sep: Union[str, None] = None, end: Union[str, None] = None, wrapping: bool = True, strace: bool = True) -> Logger: - """""" + """Pseudolog for writing to log file with level `LogLevel.DEBUG`. + + Method is invoked on a `Logger` instance and will directly write + out to a log file using the built-in `printLog2File` helper method. + The returned object is a modified instance of the `Logger` class + which stores information regarding what was written to the log + file, and what formatting was applied. The `Logger.withConsole()` + method can be invoked directly afterwards (or later) to write + the same message to the console. + """ Logger.printLog2File(*value, sep=sep, end=end, wrapping=wrapping, strace=strace) @@ -72,7 +120,16 @@ class Logger: # class redeclaration & initialisation def warn(cls, *value: object, sep: Union[str, None] = None, end: Union[str, None] = None, wrapping: bool = True, strace: bool = True) -> Logger: - """""" + """Pseudolog for writing to log file with level `LogLevel.WARN`. + + Method is invoked on a `Logger` instance and will directly write + out to a log file using the built-in `printLog2File` helper method. + The returned object is a modified instance of the `Logger` class + which stores information regarding what was written to the log + file, and what formatting was applied. The `Logger.withConsole()` + method can be invoked directly afterwards (or later) to write + the same message to the console. + """ Logger.printLog2File(*value, level=LogLevel.WARN, sep=sep, end=end, wrapping=wrapping, strace=strace) @@ -88,7 +145,16 @@ class Logger: # class redeclaration & initialisation def error(cls, *value: object, sep: Union[str, None] = None, end: Union[str, None] = None, wrapping: bool = True, strace: bool = True) -> Logger: - """""" + """Pseudolog for writing to log file with level `LogLevel.ERROR`. + + Method is invoked on a `Logger` instance and will directly write + out to a log file using the built-in `printLog2File` helper method. + The returned object is a modified instance of the `Logger` class + which stores information regarding what was written to the log + file, and what formatting was applied. The `Logger.withConsole()` + method can be invoked directly afterwards (or later) to write + the same message to the console. + """ Logger.printLog2File(*value, level=LogLevel.ERROR, sep=sep, end=end, wrapping=wrapping, strace=strace) @@ -102,31 +168,77 @@ class Logger: # class redeclaration & initialisation @staticmethod def withConsole() -> None: + """Write the last message logged to a file to the console as + well. That is, any message written using a pseudolog method. + The last message is determined by a pseudolog, which modifies + the state of the class to remember information regarding what + was recently written out to a log file. + + This method will write to the console according to the standard + PIPE of each type of logging level. Colouring will be enabled + for outputs in association to the log level. If PIPE is + redirected to external file, colouring is disabled. Any other + information regarding the formatting of the message is directly + associated to the formatting used when writing to a log file. + """ if not Logger.__loginfo is None: Logger.__printLog__(Logger.__loginfo.isatty, Logger.__loginfo.lv, Logger.__loginfo.msg, Logger.__loginfo.sep, Logger.__loginfo.end) - @staticmethod - def genLogHeader(_type: Union[LogLevel, int, str]) -> str: - ## if the _type represents a LogLevel value, convert it to str - if not isinstance(_type, str): - _type = loglevel_as_str(_type) - - ## return newly formatted log message header - return Logger.__LOG_TMPL.format('{CALLER}', DATE=\ - time.strftime(Logger.__TIMESTAMP_FMT, time.localtime()), - TYPE=_type) + "{0}" - - @staticmethod def printLog2File(*value: object, level: Union[LogLevel, int] = LogLevel.DEBUG, mode: str = 'a', file: Union[TextIOWrapper, str] = ..., sep: Union[str, None] = None, end: Union[str, None] = None, wrapping: bool = True, strace: bool = True, header: bool = True) -> None: - """""" + """Wrapper method over the built-in `print()` function defined + using 3.x syntax. All Familiar functionality can be passed to + the method as found when calling `print()`, but comes with added + features. + + Method not to be confused with `Logger.printLog()`, + `printLog2File` provides enhanced and guaranteed handling of + file streams using the built-in `with` statement. `printLog()` + can however write out to a file stream, but requires a + `TextIOWrapper` object to be given, or omitted with `None` for + output to the `STDOUT` stream. `printLog2File` can take a string + pathspec as the location to a file and open the file stream to + write into. + + This method is designed strictly to write messages to a log file + with ehanced features, such as line-wrapping and stacktrace. By + default, this method will generate a log entry header with + `strace` and `wrapping` enabled. Optionally, these can be disabled + when calling the method. If the `header` is disabled, it means the + given `value` is written directly to the log file. This allows for + process controlled messages to be written, i.e., a log might be + written employing a process is about to be performed, and append + the values 'ok' or 'failed', depending on the finishing state of + the process. + + Examples of logging to file: + ```py + >>> import clog + >>> + >>> log_file = ".dump.log" + >>> clog.Logger.printLog2File("Hello from log file!", file=log_file) + >>> # note, we can still pass a TextIOWrapper object + >>> with open(log_file, 'a') as f: + ... clog.Logger.printLog2File("Using own wapper.", file=f) + ... + >>> # process controlled logging + >>> clog.Logger.printLog2File("Establishing OS...", end="") + >>> import os + >>> clog.Logger.printLog2File(os.name, header=False) + ``` + + NOTE: if `file` is omitted when invoking method, the default + pathspec is used to write to file (defined as `__DEFAULT_OUT_FILE`). + If a new `Logger` instance was established, when `file` is obmitted, + the default pathspec used is defined by the `Logger` instance. + """ ## handle if no file parameter was given if file is Ellipsis or not os.path.exists(file): file = Logger.__DEFAULT_OUT_FILE @@ -138,7 +250,7 @@ class Logger: # class redeclaration & initialisation os.getcwd()).strip('\\\/') ## generate new header for log file and construct new message if header: - msg = Logger.genLogHeader(level).format( + msg = gen_log_header(level).format( " ".join(value), CALLER="{0}:{1}[{2}]".format( _fname, _frame.f_code.co_name, _frame.f_lineno ).replace("module", "global") if strace else "LOGGER" @@ -170,7 +282,7 @@ class Logger: # class redeclaration & initialisation the method as found when calling `print()`, but comes with added features. - `IO.printLog` is designed for purpose of logging information to + `Logger.printLog` is designed for purpose of logging information to the console window or to a file, either via an explicit write by passing a compatible `SupportsWrite[str]` value to `file=`, or by redirecting the standard PIPE streams to an external file. @@ -185,19 +297,19 @@ class Logger: # class redeclaration & initialisation formatted text to the standard stream. Log level can be elevated by either passing an integer to represent the log level, or pass an enum variable from `class LogLevel` from the - `utils/printfmt.py` module. + `utils/common.py` module. Examples of logging: ``` - >>> import utils - >>> utils.IO.printLog("Hello, World!") + >>> import clog + >>> clog.Logger.printLog("Hello, World!") Hello, World! - >>> utils.IO.printLog("Hello,", "World" + "!", level=LogLevel.DEBUG) + >>> clog.Logger.printLog("Hello,", "World" + "!", level=LogLevel.DEBUG) \033[94mHello, World!\033[0m - >>> utils.IO.printLog("Hello,", end=" ") ; utils.IO.printLog("World!", level=1) + >>> clog.Logger.printLog("Hello,", end=" ") ; clog.Logger.printLog("World!", level=1) Hello, \033[92mWorld!\033[0m >>> with open("dump.log", 'a') as log_file: - ... utils.IO.printLog("Hello, Log File!", file=log_file) + ... clog.Logger.printLog("Hello, Log File!", file=log_file) ... >>> ``` @@ -239,11 +351,15 @@ class Logger: # class redeclaration & initialisation def __printLog__(isatty: bool, lv: LogLevel, msg: object, s: Union[str, None] = None, e: Union[str, None] = None, flsh: bool = True) -> None: + """Private helper method responsible for invoking the built-in + `print` function with appropriate keyword arugments. Method + identifies the PIPE used and provide text highlighting accordingly. + """ ## handle if we have a redirect if isatty and (Logger.__stdpipe is sys.stdout or \ Logger.__stdpipe is sys.stderr): ## write ANSI code to start coloured text - print(logAsCol(lv), end="", file=Logger.__stdpipe, flush=flsh) + print(log_as_col(lv), end="", file=Logger.__stdpipe, flush=flsh) ## unpack the object and pass to print print(*msg, sep=s, end="", file=Logger.__stdpipe, flush=flsh) ## reset the colour sequence back to normal