Compare commits

...

16 Commits

Author SHA1 Message Date
65e25915c0
Updated version 2023-07-29 22:28:15 +01:00
d6afb45a73
Updated _logger.py
Fixed bug in class@debug() method where the appropriate level wasn't being passed.
2023-07-29 22:27:35 +01:00
8e6613412b
Updated _logger.py
Major changes to the structure behaviour when logging.

- Class no longer implements pseudologs as class methods. These are now
  instance methods but return the class instance to allow for method
  chaining.

- `class@Logger` is no longer a singleton pattern. Class may now either
  return a new logger instance, or return an existing logger if matched.
  A user may recreate an existing logger using the `Logger@new()` method
  instance.

- An existing logger may be returned either by creating a new logger
  with the same `name=` attribute, or by using the `Logger@get()` static
  method and passing the name of an existing logger.

- A context manager, `class@LoggerStore`, has been implemented. This is to
  allow for multiple loggers to be created and kept track of during the
  lifetime of a program. This class is fully static and only accessed by
  internal processes.

- The `Logger@printLog2File` method has been renamed to `writeLog()`. In
  the case a user wishes to write to console, `Logger@printLog` may be
  used. Note, pseudolog methods still print to log as default behaviour.

- Other changes to class methods to be appropriate for the amended
  changes outlined above.

- Bug fixes.
2023-07-29 21:15:23 +01:00
a03342d81c
Updated common.py
Changed value name from 'NORMAL' to 'INFO'.
2023-07-29 21:15:06 +01:00
3083aa9b93
Amended module exports 2023-07-29 21:13:44 +01:00
0ad7b552b2
Update pyproject.toml 2023-07-29 21:07:39 +01:00
9f5f12a978
Updated pyproject.toml 2023-07-06 20:44:32 +01:00
c0e9c91206
Updated README.md 2023-07-06 20:44:17 +01:00
0062006199
Updates testing files 2023-07-06 20:36:50 +01:00
3615d2a03b
Removed tox.ini
Contents now in pyproject.toml
2023-07-06 20:36:18 +01:00
94fd027d6f
Removed requirements-dev.txt 2023-07-06 20:35:08 +01:00
f4bf4f96b6
Updated pyproject.toml
File is now fully compliant to build project with hatchling.
Replaces: setup.py, setup.cfg, any config files have been translated to
toml format.
2023-07-06 20:31:19 +01:00
bccd6155fb
Moved clog => src/clog
Change is to be compliant with pyproject.toml and hatchling
2023-07-06 20:14:39 +01:00
1a4a8c488d
Added py.typed 2023-07-06 20:14:37 +01:00
dd565ff035
Updated .gitignore 2023-07-06 20:12:04 +01:00
da46bb2cf5
Removed setup files
Using pyproject.toml
2023-07-06 16:57:23 +01:00
18 changed files with 671 additions and 502 deletions

167
.gitignore vendored
View File

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

View File

@ -1,4 +1,4 @@
![clog logo](./assets/clog.svg)
![clog logo](https://git.closedless.xyz/ClosedLess/clog/raw/branch/main/assets/clog.svg)
# CLog ClosedLess Logger
Logging as simple as putting on a shoe.

View File

@ -1 +0,0 @@
from ._logger import Logger

View File

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

View File

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

View File

@ -1,5 +0,0 @@
flake8==4.0.1
tox==3.25.0
pytest==7.1.2
pytest-cov==3.0.0
mypy==0.950

View File

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

View File

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

@ -0,0 +1,3 @@
from clog._logger import Logger
__all__ = ['Logger']

429
src/clog/_logger.py Normal file
View 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
View File

View 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"\"'")))

View File

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

View File

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

11
tox.ini
View File

@ -1,11 +0,0 @@
[tox]
minversion = 3.25.0
envlist = py36, py37, py38, py39, py310
isolated_build = true
[testenv]
setenv =
PYTHONPATH = {toxinidir}
deps =
-r{toxinidir}/requirements-dev.txt
commands =