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.
This commit is contained in:
Ethan Smith-Coss 2022-05-03 18:13:18 +01:00
parent a0e487a2a7
commit 6ddb43d194
Signed by: TheOnePath
GPG Key ID: 4E7D436CE1A0BAF1

View File

@ -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