Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 65e25915c0 | |||
| d6afb45a73 | |||
| 8e6613412b | |||
| a03342d81c | |||
| 3083aa9b93 | |||
| 0ad7b552b2 | |||
| 9f5f12a978 | |||
| c0e9c91206 | |||
| 0062006199 | |||
| 3615d2a03b | |||
| 94fd027d6f | |||
| f4bf4f96b6 | |||
| bccd6155fb | |||
| 1a4a8c488d | |||
| dd565ff035 | |||
| da46bb2cf5 |
167
.gitignore
vendored
167
.gitignore
vendored
|
|
@ -1,7 +1,162 @@
|
|||
.tox
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.*_cache
|
||||
**py.typed**
|
||||
**__pycache__**
|
||||
**.log**
|
||||
**.egg-info**
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||

|
||||

|
||||
# CLog – ClosedLess Logger
|
||||
Logging as simple as putting on a shoe.
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
from ._logger import Logger
|
||||
371
clog/_logger.py
371
clog/_logger.py
|
|
@ -1,371 +0,0 @@
|
|||
from __future__ import annotations
|
||||
import os
|
||||
import sys
|
||||
from io import TextIOWrapper
|
||||
from typing import IO, Any, Type, Union
|
||||
from collections import namedtuple
|
||||
|
||||
from .utils import common
|
||||
from .utils import printfmt
|
||||
|
||||
|
||||
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: IO[Any] = 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 = os.path.realpath("dump.log")
|
||||
## log info namedtuple for storing class states
|
||||
__LOG_INFO_TUPLE = namedtuple('__LOG_INFO_TUPLE',
|
||||
['isatty', 'lv', 'msg', 'sep', 'end'])
|
||||
## create instance attribute for class singleton
|
||||
__instance__ = None
|
||||
## create default file instance which can change on construct
|
||||
__default_out_file = __DEFAULT_OUT_FILE
|
||||
## create instance attribute as read-only for log location
|
||||
log = __default_out_file
|
||||
|
||||
def __new__(cls, *, out_f: Union[str, None] = None) -> 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
|
||||
## handle if a custom file pathspec was given
|
||||
if out_f is not None 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: Logger.__LOG_INFO_TUPLE = Logger.__LOG_INFO_TUPLE(
|
||||
*([None] * 5)
|
||||
) # default the namedtuple to None on first instance
|
||||
|
||||
cls.printLog2File("----[New instance of script has been started]----",
|
||||
file=cls.log, mode='w')
|
||||
|
||||
return cls.__instance__
|
||||
|
||||
@classmethod
|
||||
def new(cls, *, out_f: Union[str, None] = None) -> Logger:
|
||||
cls.__instance__ = None # destroy the instance
|
||||
# reset all attributes to use the default file out
|
||||
cls.log = cls.__default_out_file =\
|
||||
cls.__DEFAULT_OUT_FILE
|
||||
|
||||
return Logger(out_f=out_f) # construct new instance and return it
|
||||
|
||||
@classmethod
|
||||
def debug(cls, *value: object, sep: Union[str, None] = None,
|
||||
end: Union[str, None] = None, wrapping: bool = True,
|
||||
strace: bool = True) -> Type[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 = 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) -> Type[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 = 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) -> Type[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 = 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, None] = None,
|
||||
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 None or not isinstance(file, (TextIOWrapper, str)):
|
||||
file = Logger.__default_out_file
|
||||
|
||||
if not isinstance(file, str):
|
||||
file = file.name
|
||||
|
||||
if 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
|
||||
_val = [*map(str, value)] # convert all objects to string
|
||||
if header:
|
||||
msg = printfmt.gen_log_header(level).format(
|
||||
" ".join(_val), CALLER="{0}:{1}[{2}]".format(
|
||||
_fname, _frame.f_code.co_name, _frame.f_lineno
|
||||
).replace("module", "global") if strace else "LOGGER"
|
||||
)
|
||||
else:
|
||||
msg = " ".join(_val)
|
||||
|
||||
# perform wrapping of message and indent wrapped lines
|
||||
if wrapping:
|
||||
msg = printfmt.wrap(msg).replace('\n', '\n\t')
|
||||
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
def printLog(*value: object,
|
||||
level: Union[int, common.LogLevel] = common.LogLevel.NORMAL,
|
||||
sep: Union[str, None] = None, end: Union[str, None] = None,
|
||||
file=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: # type: ignore
|
||||
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: # type: ignore # 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: Union[common.LogLevel, int],
|
||||
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) # type: ignore
|
||||
## 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) # type: ignore
|
||||
|
|
@ -1,6 +1,45 @@
|
|||
[build-system]
|
||||
requires = ["setuptools>=45.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "clog"
|
||||
version = "0.2.1-alpha"
|
||||
description = "Logging as simple as putting on a shoe."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.7"
|
||||
license = {file = "LICENCE.txt"}
|
||||
authors = [
|
||||
{name = "Ethan Smith-Coss", email="ethan.sc@closedless.xyz"},
|
||||
]
|
||||
maintainers = [
|
||||
{name = "Ethan Smith-Coss", email="ethan.sc@closedless.xyz"},
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Environment :: Other Environment",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
||||
"Natural Language :: English",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Topic :: System :: Logging",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
"Topic :: Utilities",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"tox>=4",
|
||||
"pytest==7.4.0",
|
||||
"pytest-cov==4.1.0",
|
||||
"mypy==1.4.1"
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "--cov=clog"
|
||||
|
|
@ -20,4 +59,35 @@
|
|||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
warn_unused_configs = true
|
||||
no_implicit_reexport = true
|
||||
no_implicit_reexport = true
|
||||
|
||||
[tool.tox]
|
||||
legacy_tox_ini = """
|
||||
[tox]
|
||||
minversion >= 4
|
||||
skip_missing_interpreters = {env:TOX_SKIP_MISSING_INTERPRETERS:True}
|
||||
env_list =
|
||||
py37,
|
||||
py38,
|
||||
py39,
|
||||
py310,
|
||||
type,
|
||||
isolated_build = true
|
||||
|
||||
[testenv]
|
||||
description = run pytest unit tests
|
||||
deps =
|
||||
pytest==7.4.0,
|
||||
pytest-cov==4.1.0,
|
||||
commands = python -m pytest {posargs}
|
||||
|
||||
[testenv:type]
|
||||
description = run typing libraries mypy
|
||||
deps = mypy==1.4.1
|
||||
commands = python -m mypy src/clog
|
||||
"""
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://closedless.xyz"
|
||||
Repository = "https://git.closedless.xyz/ClosedLess/clog"
|
||||
"Bug Tracker" = "https://git.closedless.xyz/ClosedLess/clog/issues"
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
flake8==4.0.1
|
||||
tox==3.25.0
|
||||
pytest==7.1.2
|
||||
pytest-cov==3.0.0
|
||||
mypy==0.950
|
||||
50
setup.cfg
50
setup.cfg
|
|
@ -1,50 +0,0 @@
|
|||
[metadata]
|
||||
name = clog
|
||||
description = logging as simple as putting on a shoe.
|
||||
author = Ethan Smith-Coss
|
||||
license = GNU GPLv3+
|
||||
license_file = LICENCE.txt
|
||||
platforms = unix, linux, osx, cygwin, win32
|
||||
classifiers =
|
||||
Development Status :: 3 - Alpha
|
||||
Environment :: Other Environment
|
||||
Intended Audience :: Developers
|
||||
License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
||||
Natural Language :: English
|
||||
Operating System :: OS Independent
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
Programming Language :: Python :: 3 :: Only
|
||||
Topic :: System :: Logging
|
||||
Topic :: Software Development :: Libraries
|
||||
Topic :: Utilities
|
||||
|
||||
[options]
|
||||
python_requires = >=3.6, <4
|
||||
package_dir =
|
||||
=clog
|
||||
zip_safe = no
|
||||
|
||||
[options.extras_require]
|
||||
testing =
|
||||
pytest>=7.0
|
||||
pytest-cov>=3.0
|
||||
mypy>=0.950
|
||||
flake8>=4.0
|
||||
tox>=3.25
|
||||
|
||||
[options.package_data]
|
||||
clog = py.typed
|
||||
|
||||
[flake8]
|
||||
ignore = E266, E128, E261, E221, W292, W503
|
||||
max-line-length = 120
|
||||
per-file-ignores =
|
||||
clog/__init__.py:F401, F403
|
||||
clog/utils/__init__.py:F401, F403
|
||||
clog/_logger.py:E302, E701
|
||||
setup.py:E126
|
||||
50
setup.py
50
setup.py
|
|
@ -1,50 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict
|
||||
from setuptools import setup
|
||||
|
||||
CURRENT_PYVERSION = sys.version_info[:2]
|
||||
REQUIRED_PYVERSION = (3, 6)
|
||||
|
||||
if CURRENT_PYVERSION < REQUIRED_PYVERSION:
|
||||
sys.stderr.write(
|
||||
"""
|
||||
==================================
|
||||
Whoops, unsupported Python version
|
||||
==================================
|
||||
It's seems as though your foot size does not meet the minimum required
|
||||
shoe size for clog. Your foot size must at least be {}.{}, otherwise the
|
||||
shoe will just fall off - and nobody wants to smell you stinky feet!
|
||||
|
||||
Your current foot size is {}.{} which you are trying to put the shoe on.
|
||||
If you wish to resolve this, consider growing your feet from the
|
||||
official Python website to the minimum required version.
|
||||
|
||||
...just make sure there aren't any snakes in your shoes afterwards.
|
||||
""".format(
|
||||
*(REQUIRED_PYVERSION + CURRENT_PYVERSION)
|
||||
)
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
info: Dict[str, str] = {}
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
with open(os.path.join(here, "clog", "__version__.py"), "r", encoding="utf-8") as f:
|
||||
exec(f.read(), info)
|
||||
|
||||
with open("README.md", 'r', encoding="utf-8") as f:
|
||||
readme = f.read()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
setup(
|
||||
version=info['__version__'],
|
||||
long_description=readme,
|
||||
long_description_content_type="text/markdown",
|
||||
url=info['__url__'],
|
||||
include_package_data=True,
|
||||
author_email=info['__author_email__'],
|
||||
package_data={"": ["LICENCE.txt"]}
|
||||
)
|
||||
3
src/clog/__init__.py
Normal file
3
src/clog/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from clog._logger import Logger
|
||||
|
||||
__all__ = ['Logger']
|
||||
429
src/clog/_logger.py
Normal file
429
src/clog/_logger.py
Normal file
|
|
@ -0,0 +1,429 @@
|
|||
from __future__ import annotations
|
||||
import os
|
||||
import sys
|
||||
import random
|
||||
import string
|
||||
from io import TextIOWrapper
|
||||
from typing import IO, Any, Union
|
||||
from collections import namedtuple
|
||||
|
||||
from clog.utils import common
|
||||
from clog.utils import printfmt
|
||||
|
||||
#class A:
|
||||
# """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
|
||||
STD_PIPE: IO[Any] = 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
|
||||
## log info namedtuple for storing class states
|
||||
LOG_INFO_TUPLE = namedtuple('__LOG_INFO_TUPLE', ['isatty', 'lv', 'msg', 'sep', 'end'])
|
||||
|
||||
|
||||
class LoggerStore(dict):
|
||||
__logger_store: dict[str, Logger] = {}
|
||||
|
||||
|
||||
def __getitem__(self, __key: str) -> Logger:
|
||||
logger = __class__.__logger_store[__key]
|
||||
# perform the lookup and allow for Python to raise errors appropriately
|
||||
return logger
|
||||
|
||||
|
||||
@staticmethod
|
||||
def add(name: str, instance: Logger, *, _force = False) -> Logger:
|
||||
if not name in __class__.__logger_store or _force:
|
||||
__class__.__logger_store[name] = instance
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
@staticmethod
|
||||
def rm(name: str, *, _all = False) -> None:
|
||||
if name in __class__.__logger_store:
|
||||
del __class__.__logger_store[name]
|
||||
|
||||
if _all:
|
||||
for key in __class__.__logger_store:
|
||||
del __class__.__logger_store[key]
|
||||
|
||||
|
||||
@staticmethod
|
||||
def lookup(name: str) -> bool:
|
||||
return True if name in __class__.__logger_store else False
|
||||
|
||||
|
||||
@staticmethod
|
||||
def getitem(name: str) -> Logger:
|
||||
return __class__()[name]
|
||||
|
||||
|
||||
class Logger:
|
||||
def __new__(cls, *_, **kwargs):
|
||||
"""Construct a new instance of the class and initialise it.
|
||||
|
||||
Constructor method is used to establish the class as a
|
||||
Factory pattern. A new instance is returned from
|
||||
the constructor, or if an existing instance is present,
|
||||
return the object of that instance.
|
||||
"""
|
||||
if 'name' in kwargs and LoggerStore.lookup(kwargs['name']):
|
||||
return LoggerStore.getitem(kwargs['name'])
|
||||
|
||||
cls.__tmp_store_id = str(id(cls))
|
||||
LoggerStore.add(cls.__tmp_store_id, super(Logger, cls).__new__(cls))
|
||||
|
||||
return LoggerStore.getitem(cls.__tmp_store_id)
|
||||
|
||||
|
||||
def __init__(self, *, out_file: Union[str, None] = None, name: str = "", **_) -> None:
|
||||
# if the temporary ID attribute isn't set, then we were accessing an existing logger, we can exit init
|
||||
try:
|
||||
__class__.__tmp_store_id
|
||||
except AttributeError:
|
||||
return
|
||||
|
||||
# if a name wasn't specified, use a random sequence of length 16
|
||||
self._name = name or ''.join(random.choices(string.ascii_lowercase + string.digits, k=16))
|
||||
|
||||
# fetch the new logger and delete the temporary ID
|
||||
store = LoggerStore.getitem(__class__.__tmp_store_id)
|
||||
LoggerStore.rm(__class__.__tmp_store_id)
|
||||
# add the logger back using the new name
|
||||
LoggerStore.add(self._name, store)
|
||||
# remove the temporary ID attribute
|
||||
del __class__.__tmp_store_id
|
||||
|
||||
self._log = os.path.realpath(f".{self._name}.log")
|
||||
## handle if a custom file pathspec was given
|
||||
if out_file is not None and isinstance(out_file, str):
|
||||
## verify path and convert to real pathspec.
|
||||
if common.is_path_spec(out_file):
|
||||
## redefine the default log out attribute and create
|
||||
## public attribute for the currect log location
|
||||
self._log = os.path.realpath(out_file)
|
||||
|
||||
# default the namedtuple to None on first instance
|
||||
self.__log_info = LOG_INFO_TUPLE(*([None] * 5))
|
||||
self.__std_pipe = STD_PIPE
|
||||
|
||||
self.writeLog("----[New instance of script has been started]----", mode='w')
|
||||
|
||||
|
||||
@property
|
||||
def log(self) -> str:
|
||||
return self._log
|
||||
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
|
||||
def new(self, *, out_file: Union[str, None] = None) -> Logger:
|
||||
del LoggerStore()[self.name]
|
||||
return type(self)(out_file=out_file, name=self.name) # construct new instance and return it
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get(name) -> Logger:
|
||||
if not LoggerStore.lookup(name):
|
||||
raise LookupError(f"Cannot find logger {repr(name)} in store.")
|
||||
|
||||
return LoggerStore()[name]
|
||||
|
||||
|
||||
def info(self, *value: object, sep: Union[str, None] = None, end: Union[str, None] = None, wrapping: bool = True,
|
||||
strace: bool = False) -> 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.
|
||||
"""
|
||||
self.writeLog(*value, sep=sep, end=end, wrapping=wrapping, strace=strace)
|
||||
|
||||
self.__std_pipe = sys.stdout
|
||||
self.__log_info = LOG_INFO_TUPLE(IS_STDOUT_REDIR, common.LogLevel.INFO, value, sep, end)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
def debug(self, *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.
|
||||
"""
|
||||
self.writeLog(*value, level=common.LogLevel.DEBUG, sep=sep, end=end, wrapping=wrapping, strace=strace)
|
||||
|
||||
self.__std_pipe = sys.stderr
|
||||
self.__log_info = LOG_INFO_TUPLE(IS_STDERR_REDIR, common.LogLevel.DEBUG, value, sep, end)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
def warn(self, *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.
|
||||
"""
|
||||
self.writeLog(*value, level=common.LogLevel.WARN, sep=sep, end=end, wrapping=wrapping, strace=strace)
|
||||
|
||||
self.__std_pipe = sys.stderr
|
||||
self.__log_info = LOG_INFO_TUPLE(IS_STDERR_REDIR, common.LogLevel.WARN, value, sep, end)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
def error(self, *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.
|
||||
"""
|
||||
self.writeLog(*value, level=common.LogLevel.ERROR, sep=sep, end=end, wrapping=wrapping, strace=strace)
|
||||
|
||||
self.__std_pipe = sys.stderr
|
||||
self.__log_info = LOG_INFO_TUPLE(IS_STDERR_REDIR, common.LogLevel.ERROR, value, sep, end)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
def withConsole(self) -> 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 self.__log_info is not None:
|
||||
self.__printLog__(self.__log_info.isatty, self.__log_info.lv, self.__log_info.msg,
|
||||
self.__log_info.sep, self.__log_info.end
|
||||
)
|
||||
|
||||
|
||||
def writeLog(self, *value: object, level: Union[common.LogLevel, int] = common.LogLevel.INFO, mode: str = 'a',
|
||||
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.self.writeLog("Hello from log file!", file=log_file)
|
||||
>>> # note, we can still pass a TextIOWrapper object
|
||||
>>> with open(log_file, 'a') as f:
|
||||
... clog.self.writeLog("Using own wapper.", file=f)
|
||||
...
|
||||
>>> # process controlled logging
|
||||
>>> clog.self.writeLog("Establishing OS...", end="")
|
||||
>>> import os
|
||||
>>> clog.self.writeLog(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.
|
||||
"""
|
||||
_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.lstrip(
|
||||
os.getcwd()).strip('\\/')
|
||||
## generate new header for log file and construct new message
|
||||
_val = [*map(str, value)] # convert all objects to string
|
||||
if header:
|
||||
msg = printfmt.gen_log_header(level).format(
|
||||
" ".join(_val), CALLER="{0}:{1}[{2}]".format(
|
||||
_fname, _frame.f_code.co_name, _frame.f_lineno
|
||||
).replace("module", "global") if strace else "LOGGER"
|
||||
)
|
||||
else:
|
||||
msg = " ".join(_val)
|
||||
|
||||
# perform wrapping of message and indent wrapped lines
|
||||
if wrapping:
|
||||
msg = printfmt.wrap(msg).replace('\n', '\n\t')
|
||||
|
||||
with open(self.log, mode, encoding="utf-8") as fp:
|
||||
self.__std_pipe = fp # pre-requisite to write PIPE to file
|
||||
self.__printLog__(False, level, (msg,), sep, end, False)
|
||||
|
||||
|
||||
def printLog(self, *value: object, level: Union[int, common.LogLevel] = common.LogLevel.INFO,
|
||||
sep: Union[str, None] = None, end: Union[str, None] = None, file=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: # type: ignore
|
||||
self.__std_pipe = sys.stderr
|
||||
else:
|
||||
self.__std_pipe = sys.stdout
|
||||
## handle if the file is a TextIOWrapper
|
||||
elif isinstance(file, TextIOWrapper):
|
||||
self.__std_pipe = file # `print` will handle this as is.
|
||||
## otherwise the method was given an invalid argument
|
||||
else:
|
||||
self.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
|
||||
self.__printLog__(IS_STDOUT_REDIR or IS_STDERR_REDIR, level, value, sep, end, flush)
|
||||
|
||||
|
||||
def __printLog__(self, isatty: bool, lv: Union[common.LogLevel, int], 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 (self.__std_pipe is sys.stdout or self.__std_pipe is sys.stderr):
|
||||
## write ANSI code to start coloured text
|
||||
print(printfmt.log_as_col(lv), end="", file=self.__std_pipe, flush=flsh)
|
||||
## unpack the object and pass to print
|
||||
print(*msg, sep=s, end="", file=self.__std_pipe, flush=flsh) # type: ignore
|
||||
## reset the colour sequence back to normal
|
||||
print(printfmt.Colours.NORMAL, end=e, file=self.__std_pipe, flush=flsh)
|
||||
else:
|
||||
print(*msg, sep=s, end=e, file=self.__std_pipe, flush=flsh) # type: ignore
|
||||
0
src/clog/py.typed
Normal file
0
src/clog/py.typed
Normal file
|
|
@ -14,7 +14,7 @@ class LogLevel:
|
|||
|
||||
Higher `int` value means higher severity of level for logging.
|
||||
"""
|
||||
NORMAL = 0
|
||||
INFO = 0
|
||||
PASS = 1
|
||||
DEBUG = 2
|
||||
WARN = 3
|
||||
|
|
@ -49,4 +49,4 @@ def is_path_spec(path_spec: str) -> bool:
|
|||
## perform a regex match to ensure that the given path is a
|
||||
## valid pathspec for the system.
|
||||
return bool(re.match(__REGEX_PAT, path_spec.strip(r"\"'"))
|
||||
or re.match(r'^[\w\d\-_\.]+$', path_spec.strip(r"\"'")))
|
||||
or re.match(r'^[\w\d\-_\.]+$', path_spec.strip(r"\"'")))
|
||||
|
|
@ -5,7 +5,7 @@ import re
|
|||
import pytest
|
||||
|
||||
from clog import Logger
|
||||
from utils.common import LogLevel
|
||||
from clog.utils.common import LogLevel
|
||||
|
||||
|
||||
logger = Logger()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import re
|
||||
|
||||
import pytest
|
||||
from utils.common import LogLevel
|
||||
from clog.utils.common import LogLevel
|
||||
|
||||
from clog.utils.printfmt import (
|
||||
Colours,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user