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.
368 lines
16 KiB
Python
368 lines
16 KiB
Python
import os
|
|
import sys
|
|
from io import TextIOWrapper
|
|
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
|
|
__IS_STDOUT_REDIR: bool = os.isatty(sys.stdout.fileno())
|
|
__IS_STDERR_REDIR: bool = os.isatty(sys.stderr.fileno())
|
|
## the default log out file
|
|
__DEFAULT_OUT_FILE: str = "dump.log"
|
|
## log info namedtuple for storing class states
|
|
__LOG_INFO_TUPLE = namedtuple('LogInfo',
|
|
['isatty', 'lv', 'msg', 'sep', 'end'])
|
|
## create instance attribute for class singleton
|
|
__instance__ = None
|
|
## create instance attribute as read-only for log location
|
|
log = None
|
|
|
|
|
|
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.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 = \
|
|
os.path.realpath(out_f).strip('"')
|
|
|
|
cls.__loginfo = None # default the namedtuple to None on first instance
|
|
|
|
cls.printLog2File("----[New instance of script has been started]----",
|
|
file=cls.__DEFAULT_OUT_FILE, mode='w')
|
|
|
|
return cls.__instance__
|
|
|
|
|
|
@classmethod
|
|
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)
|
|
|
|
|
|
Logger.__stdpipe = sys.stderr
|
|
cls.__loginfo: namedtuple = Logger.__LOG_INFO_TUPLE(
|
|
Logger.__IS_STDERR_REDIR, LogLevel.DEBUG, value, sep, end
|
|
)
|
|
|
|
return cls
|
|
|
|
|
|
@classmethod
|
|
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)
|
|
|
|
Logger.__stdpipe = sys.stderr
|
|
cls.__loginfo: namedtuple = Logger.__LOG_INFO_TUPLE(
|
|
Logger.__IS_STDERR_REDIR, LogLevel.WARN, value, sep, end
|
|
)
|
|
|
|
return cls
|
|
|
|
|
|
@classmethod
|
|
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)
|
|
|
|
Logger.__stdpipe = sys.stderr
|
|
cls.__loginfo: namedtuple = Logger.__LOG_INFO_TUPLE(
|
|
Logger.__IS_STDERR_REDIR, LogLevel.ERROR, value, sep, end
|
|
)
|
|
|
|
return cls
|
|
|
|
|
|
@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 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
|
|
|
|
_frame = sys._getframe(2) if sys._getframe(1).f_code.co_name in \
|
|
dir(Logger) else sys._getframe(1)
|
|
## get the executing filename of where log was called
|
|
_fname = _frame.f_code.co_filename.removeprefix(
|
|
os.getcwd()).strip('\\\/')
|
|
## generate new header for log file and construct new message
|
|
if header:
|
|
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"
|
|
)
|
|
else:
|
|
msg = " ".join(value)
|
|
|
|
# perform wrapping of message and indent wrapped lines
|
|
if wrapping:
|
|
msg = wrap(msg).replace('\n', '\n\t')
|
|
|
|
|
|
if isinstance(file, str):
|
|
with open(file, mode, encoding="utf-8") as log:
|
|
Logger.__stdpipe = log # pre-requisite to write PIPE to file
|
|
Logger.__printLog__(False, level, (msg,), sep, end, False)
|
|
elif isinstance(file, TextIOWrapper):
|
|
Logger.__stdpipe = log
|
|
Logger.__printLog__(False, level, (msg,), sep, end, False)
|
|
|
|
|
|
@staticmethod
|
|
def printLog(*value: object, level: Union[int, LogLevel] = LogLevel.NORMAL,
|
|
sep: Union[str, None] = None, end: Union[str, None] = None,
|
|
file: Union[TextIOWrapper, None] = None,
|
|
flush: 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.
|
|
|
|
`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.
|
|
In addition, different levels of logging will result in output
|
|
to standard PIPE streams to have appropriate highlighting to the
|
|
message displayed. If standard PIPE streams are to be redirected
|
|
to an external file via a PIPE redirect, the highlighting syntax
|
|
is dropped do prevent ANSI escape code sequences from being
|
|
written to file.
|
|
|
|
The standard log level is `NORMAL`, referring to standard
|
|
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/common.py` module.
|
|
|
|
Examples of logging:
|
|
```
|
|
>>> import clog
|
|
>>> clog.Logger.printLog("Hello, World!")
|
|
Hello, World!
|
|
>>> clog.Logger.printLog("Hello,", "World" + "!", level=LogLevel.DEBUG)
|
|
\033[94mHello, World!\033[0m
|
|
>>> clog.Logger.printLog("Hello,", end=" ") ; clog.Logger.printLog("World!", level=1)
|
|
Hello, \033[92mWorld!\033[0m
|
|
>>> with open("dump.log", 'a') as log_file:
|
|
... clog.Logger.printLog("Hello, Log File!", file=log_file)
|
|
...
|
|
>>>
|
|
```
|
|
"""
|
|
### :@Ethan: whilst `print` disables force flushing of the
|
|
# stream, from testing, it's best to forcibly flush the stream
|
|
# so the default behaviour is to do exactly this.
|
|
|
|
## configure PIPE to STDERR if logging is high enough
|
|
if file is None:
|
|
if level >= LogLevel.DEBUG:
|
|
Logger.__stdpipe = sys.stderr
|
|
else:
|
|
Logger.__stdpipe = sys.stdout
|
|
## handle if the file is a TextIOWrapper
|
|
elif isinstance(file, TextIOWrapper):
|
|
Logger.__stdpipe = file # `print` will handle this as is.
|
|
## otherwise the method was given an invalid argument
|
|
else:
|
|
Logger.printLog("Warning: logging function was called with a",
|
|
"file specifier parameter which is not a valid option.",
|
|
level=LogLevel.WARN)
|
|
return
|
|
|
|
|
|
## display message to console with appropriate colouring
|
|
### :@NOTE: if there's a PIPE redirect, don't use colour
|
|
### for that redirect PIPE
|
|
|
|
if level < LogLevel.WARN: ## handle output for STDOUT
|
|
Logger.__printLog__(Logger.__IS_STDOUT_REDIR, level, value,
|
|
sep, end, flush)
|
|
else: ## handle output for STDERR
|
|
Logger.__printLog__(Logger.__IS_STDERR_REDIR, level, value,
|
|
sep, end, flush)
|
|
|
|
|
|
@staticmethod
|
|
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(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
|
|
print(Colours.NORMAL, end=e, file=Logger.__stdpipe, flush=flsh)
|
|
else:
|
|
print(*msg, sep=s, end=e, file=Logger.__stdpipe, flush=flsh) |