This commit is a continuation from commit [3746b773f4] which is a separation of library modules into its own package from a larger project. Version control jump is significant in feature implementations due simply to under-using commit control after minor feature implementations. Project will receive better commit messages going forward on changes to the package.
252 lines
10 KiB
Python
252 lines
10 KiB
Python
from collections import namedtuple
|
|
import os
|
|
import sys
|
|
import time
|
|
from io import TextIOWrapper
|
|
from typing import TextIO, Union, TYPE_CHECKING
|
|
|
|
from utils import common
|
|
from utils.printfmt import *
|
|
|
|
class Logger: ... # class delcaration
|
|
class Logger: # class redeclaration & initialisation
|
|
## 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 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'])
|
|
## 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:
|
|
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):
|
|
## 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:
|
|
""""""
|
|
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:
|
|
""""""
|
|
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:
|
|
""""""
|
|
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:
|
|
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:
|
|
""""""
|
|
## 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 = Logger.genLogHeader(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.
|
|
|
|
`IO.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/printfmt.py` module.
|
|
|
|
Examples of logging:
|
|
```
|
|
>>> import utils
|
|
>>> utils.IO.printLog("Hello, World!")
|
|
Hello, World!
|
|
>>> utils.IO.printLog("Hello,", "World" + "!", level=LogLevel.DEBUG)
|
|
\033[94mHello, World!\033[0m
|
|
>>> utils.IO.printLog("Hello,", end=" ") ; utils.IO.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)
|
|
...
|
|
>>>
|
|
```
|
|
"""
|
|
### :@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:
|
|
## 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)
|
|
## 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) |