Files have been changed to ensure they abid flake8 defined rules that have not been identified as ignored. Files mostly pass flake8 rules.
358 lines
16 KiB
Python
358 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 import printfmt
|
|
|
|
|
|
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, common.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=common.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, common.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=common.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, common.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 Logger.__loginfo is not 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[common.LogLevel, int] = common.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 = printfmt.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 = printfmt.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, common.LogLevel] = common.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 >= common.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=common.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 < common.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: common.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(printfmt.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(printfmt.Colours.NORMAL, end=e, file=Logger.__stdpipe, flush=flsh)
|
|
else:
|
|
print(*msg, sep=s, end=e, file=Logger.__stdpipe, flush=flsh) |