Initial commit
This commit is contained in:
1
Bot/logger/__init__.py
Normal file
1
Bot/logger/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .logger import * # noqa
|
||||
BIN
Bot/logger/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
Bot/logger/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
Bot/logger/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
Bot/logger/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
Bot/logger/__pycache__/logger.cpython-311.pyc
Normal file
BIN
Bot/logger/__pycache__/logger.cpython-311.pyc
Normal file
Binary file not shown.
BIN
Bot/logger/__pycache__/logger.cpython-39.pyc
Normal file
BIN
Bot/logger/__pycache__/logger.cpython-39.pyc
Normal file
Binary file not shown.
320
Bot/logger/logger.py
Normal file
320
Bot/logger/logger.py
Normal file
@@ -0,0 +1,320 @@
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import sys
|
||||
from enum import Enum
|
||||
from inspect import currentframe
|
||||
from typing import (
|
||||
Optional,
|
||||
Tuple,
|
||||
Dict,
|
||||
List,
|
||||
Any,
|
||||
)
|
||||
|
||||
__name__ = 'testing'
|
||||
|
||||
__all__ = [
|
||||
'UnhandledLogError',
|
||||
'get_color',
|
||||
'Logger',
|
||||
]
|
||||
|
||||
|
||||
class Color(Enum):
|
||||
# Color end string, color reset
|
||||
RESET = "\033[0m"
|
||||
# Regular Colors. Normal color, no bold, background color etc.
|
||||
BLACK = "\033[0;30m" # BLACK
|
||||
RED = "\033[0;31m" # RED
|
||||
GREEN = "\033[0;32m" # GREEN
|
||||
YELLOW = "\033[0;33m" # YELLOW
|
||||
BLUE = "\033[0;34m" # BLUE
|
||||
MAGENTA = "\033[0;35m" # MAGENTA
|
||||
CYAN = "\033[0;36m" # CYAN
|
||||
WHITE = "\033[0;37m" # WHITE
|
||||
# Bold colors
|
||||
BLACK_BOLD = "\033[1;30m" # BLACK
|
||||
RED_BOLD = "\033[1;31m" # RED
|
||||
GREEN_BOLD = "\033[1;32m" # GREEN
|
||||
YELLOW_BOLD = "\033[1;33m" # YELLOW
|
||||
BLUE_BOLD = "\033[1;34m" # BLUE
|
||||
MAGENTA_BOLD = "\033[1;35m" # MAGENTA
|
||||
CYAN_BOLD = "\033[1;36m" # CYAN
|
||||
WHITE_BOLD = "\033[1;37m" # WHITE
|
||||
|
||||
|
||||
def get_color(color: str) -> str:
|
||||
"""Colors:
|
||||
```
|
||||
(
|
||||
"RESET",
|
||||
"BLACK",
|
||||
"RED",
|
||||
"GREEN",
|
||||
"YELLOW",
|
||||
"BLUE",
|
||||
"MAGENTA",
|
||||
"CYAN",
|
||||
"WHITE",
|
||||
"BLACK_BOLD",
|
||||
"RED_BOLD",
|
||||
"GREEN_BOLD",
|
||||
"YELLOW_BOLD",
|
||||
"BLUE_BOLD",
|
||||
"MAGENTA_BOLD",
|
||||
"CYAN_BOLD",
|
||||
"WHITE_BOLD",
|
||||
)
|
||||
```
|
||||
"""
|
||||
return {i.name: i.value for i in Color}[color.upper()]
|
||||
|
||||
|
||||
class Config:
|
||||
_INSTANCE = 0
|
||||
|
||||
def __init__(self, level: int) -> None:
|
||||
Config._INSTANCE += 1
|
||||
self._INSTANCE = Config._INSTANCE
|
||||
self._settings = {
|
||||
'success': 2,
|
||||
'info': 3,
|
||||
'custom': 2,
|
||||
'warning': 1,
|
||||
'error': 1,
|
||||
'debug': 2,
|
||||
}
|
||||
self.level = level
|
||||
self._iter = 0
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"<{self.__class__.__name__}({self._settings})>"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__}(settings={self._settings},\
|
||||
level={self._level} instance={self._INSTANCE})>"
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self._settings)
|
||||
|
||||
def __getitem__(self, k: str) -> int:
|
||||
return self._settings[k]
|
||||
|
||||
def __int__(self) -> int:
|
||||
return self.level
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._settings)
|
||||
|
||||
def __contains__(self, __o: Any) -> bool:
|
||||
return __o in self._settings
|
||||
|
||||
def __iter__(self) -> Config:
|
||||
return self
|
||||
|
||||
def __next__(self) -> str:
|
||||
keys = self.keys()
|
||||
if self._iter >= len(self):
|
||||
self._iter = 0
|
||||
raise StopIteration
|
||||
retval = keys[self._iter]
|
||||
self._iter += 1
|
||||
return retval
|
||||
|
||||
@property
|
||||
def settings(self) -> Dict[str, int]:
|
||||
return self._settings
|
||||
|
||||
@property
|
||||
def level(self) -> int:
|
||||
return self._level
|
||||
|
||||
@level.setter
|
||||
def level(self, v: int) -> None:
|
||||
"""Level setter validator
|
||||
|
||||
:param v: The level to change to
|
||||
:type v: int
|
||||
:raises ValueError: If the v is invalid for `level`
|
||||
"""
|
||||
if not 0 < v <= 5:
|
||||
raise ValueError(f"Level must be between `0-5` not `{v}`")
|
||||
self._level = v
|
||||
|
||||
def items(self): # type: ignore
|
||||
yield self._settings.items()
|
||||
|
||||
def keys(self) -> List[str]:
|
||||
return list(self._settings.keys())
|
||||
|
||||
def values(self) -> List[int]:
|
||||
return list(self._settings.values())
|
||||
|
||||
def update(self, **settings: int) -> None:
|
||||
"""Change the settings configuration for a given instance
|
||||
|
||||
:raises ValueError: If the configuraion does not exist or\
|
||||
user tries to update to an invalid value
|
||||
"""
|
||||
if all(key in self.settings for key in settings) and\
|
||||
all(0 < settings[key] <= 5 for key in settings):
|
||||
self._settings.update(settings)
|
||||
else:
|
||||
raise ValueError(f"Invalid key or value in {settings}. Remember,\
|
||||
key has to exists in {self.settings} and all values have to be between (1-5)")
|
||||
|
||||
def get(self, key: str) -> int:
|
||||
"""Get a setting configuration
|
||||
|
||||
:param key: Key of the configuration
|
||||
:type key: str
|
||||
:return: The level this configuration is set to
|
||||
:rtype: int
|
||||
"""
|
||||
return self._settings[key]
|
||||
|
||||
|
||||
class UnhandledLogError(Exception): ...
|
||||
|
||||
|
||||
class MetaLogger(type):
|
||||
"""A simple metaclass that checks if all logs/settings are correctly
|
||||
handled by comaring the ammount of settings in Config._settings agnainst
|
||||
the functions that live in Logger() - some unnecessary
|
||||
"""
|
||||
def __new__(self, name: str, bases:
|
||||
Tuple[type], attrs: Dict[str, Any]) -> type:
|
||||
error_msg = "We either have a log function that is not in settings OR\
|
||||
a function that needs to be dissmissed OR a setting that is\
|
||||
not added as a log function"
|
||||
|
||||
log_functions = 0
|
||||
target_log_functions = len(Config(1))
|
||||
dismiss_attrs = ('settings', 'get_line_info')
|
||||
for log in attrs:
|
||||
if not log.startswith('_') and log not in dismiss_attrs:
|
||||
log_functions += 1
|
||||
if not log_functions == target_log_functions:
|
||||
raise UnhandledLogError(error_msg)
|
||||
return type(name, bases, attrs)
|
||||
|
||||
|
||||
class Logger(metaclass=MetaLogger):
|
||||
"""The Logger class handles debbuging with colored information
|
||||
based on the level. Each instance has its own settings which the
|
||||
user can change independently through `self.settings.update()`.
|
||||
Mind that level 1 will print evetything and level 5 less
|
||||
"""
|
||||
def __init__(self, level: int = 2, log_path: Optional[str] = None):
|
||||
"""Initializer of Logger object
|
||||
|
||||
:param level: The level of debugging. Based on that, some informations\
|
||||
can be configured to not show up thus lowering the verbosity, defaults to 2
|
||||
:type level: int, optional
|
||||
"""
|
||||
self._settings = Config(level)
|
||||
self._log_path = log_path
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"<{self.__class__.__name__}Object-{self._settings._INSTANCE}>"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__}(instance={self._settings._INSTANCE})>"
|
||||
|
||||
@property
|
||||
def settings(self) -> Config:
|
||||
"""Getter so self._settings cannot be writable
|
||||
|
||||
:return: Config instance
|
||||
:rtype: Config
|
||||
"""
|
||||
return self._settings
|
||||
|
||||
def _log(self, header: str, msg: str) -> None:
|
||||
"""If a file log_path is provided in `Logger.__init__`
|
||||
|
||||
:param header: The msg header WARNING/ERROR ...
|
||||
:type header: str
|
||||
:param msg: The message to be logged
|
||||
:type msg: str
|
||||
"""
|
||||
if self._log_path is not None:
|
||||
if not os.path.exists(self._log_path):
|
||||
with open(self._log_path, mode='w') as _: ...
|
||||
with open(self._log_path, mode='a') as f:
|
||||
f.write(f"[{header.upper()}]: {msg}\n")
|
||||
|
||||
def _runner(self, func_name: str) -> bool:
|
||||
"""Use to check the `self.level` before printing the log
|
||||
|
||||
:param func_name: Name of the function as a string
|
||||
:type func_name: str
|
||||
:return: Wheather the settings allow this certain function to print
|
||||
:rtype: bool
|
||||
"""
|
||||
return self.settings.level <= self.settings[func_name]
|
||||
|
||||
def get_line_info(self, file: str, prompt: str) -> str:
|
||||
"""Get the file and the line of where this function is called
|
||||
|
||||
:param file: The file where the func is called (Recommended: `__file__`)
|
||||
:type file: str
|
||||
:param prompt: Any message to follow after line info
|
||||
:type prompt: str
|
||||
:return: *file path*, *line number* *prompt*
|
||||
:rtype: str
|
||||
"""
|
||||
cf = currentframe()
|
||||
msg = f"File \"{file}\", line {cf.f_back.f_lineno}" # type: ignore
|
||||
self.debug(f"{msg} : {prompt}")
|
||||
print(file)
|
||||
return msg
|
||||
|
||||
# ONLY LOGGING FUNCTIONS AFTER THIS
|
||||
def custom(self, msg: str, header: str = 'custom',
|
||||
*args: Any, color: str = get_color('reset'), **kwargs: Any) -> None:
|
||||
func_name = sys._getframe().f_code.co_name
|
||||
if self._runner(func_name):
|
||||
print(f"{color}[{header.upper()}]: {msg}{get_color('reset')}", *args, end='', **kwargs)
|
||||
print(get_color('reset'))
|
||||
self._log(header, msg)
|
||||
|
||||
def info(self, msg: str, *args: Any, **kwargs: Any) -> None:
|
||||
func_name = sys._getframe().f_code.co_name
|
||||
if self._runner(func_name):
|
||||
print(f"{Color.YELLOW.value}[{func_name.upper()}]: {msg}",
|
||||
*args, end='', **kwargs)
|
||||
print(get_color('reset'))
|
||||
self._log(func_name, msg)
|
||||
|
||||
def success(self, msg: str, *args: Any, **kwargs: Any) -> None:
|
||||
func_name = sys._getframe().f_code.co_name
|
||||
if self._runner(func_name):
|
||||
print(f"{Color.GREEN.value}[{func_name.upper()}]: {msg}",
|
||||
*args, end='', **kwargs)
|
||||
print(get_color('reset'))
|
||||
self._log(func_name, msg)
|
||||
|
||||
def warning(self, msg: str, *args: Any, **kwargs: Any) -> None:
|
||||
func_name = sys._getframe().f_code.co_name
|
||||
if self._runner(func_name):
|
||||
print(f"{Color.RED.value}[{func_name.upper()}]: {msg}",
|
||||
*args, end='', **kwargs)
|
||||
print(get_color('reset'))
|
||||
self._log(func_name, msg)
|
||||
|
||||
def error(self, msg: str, *args: Any, **kwargs: Any) -> None:
|
||||
func_name = sys._getframe().f_code.co_name
|
||||
if self._runner(func_name):
|
||||
print(f"{Color.RED_BOLD.value}[{func_name.upper()}]: {msg}",
|
||||
*args, end='', **kwargs)
|
||||
print(get_color('reset'))
|
||||
self._log(func_name, msg)
|
||||
|
||||
def debug(self, msg: str, *args: Any, **kwargs: Any) -> None:
|
||||
func_name = sys._getframe().f_code.co_name
|
||||
if self._runner(func_name):
|
||||
print(f"{Color.BLUE.value}[{func_name.upper()}]: {msg}",
|
||||
*args, end='', **kwargs)
|
||||
print(get_color('reset'))
|
||||
self._log(func_name, msg)
|
||||
123
Bot/logger/tests.py
Normal file
123
Bot/logger/tests.py
Normal file
@@ -0,0 +1,123 @@
|
||||
import os
|
||||
import io
|
||||
import sys
|
||||
import unittest
|
||||
import unittest.mock
|
||||
from pathlib import Path
|
||||
from logger import Logger
|
||||
from .logger import Config
|
||||
from typing import Any
|
||||
|
||||
|
||||
BASE_DIR = Path(__file__).parent
|
||||
|
||||
|
||||
class TestLogger(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.logger = Logger(1)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.logger = Logger(1)
|
||||
|
||||
def test_invalid_instance(self) -> None:
|
||||
with self.assertRaises(ValueError, msg='An instance with level=0 is created'):
|
||||
Logger(0)
|
||||
|
||||
with self.assertRaises(ValueError, msg='An instance with level=6 is created'):
|
||||
Logger(6)
|
||||
|
||||
logger = Logger(2)
|
||||
with self.assertRaises(ValueError, msg='A settings instance changed level above limit after creation'): # noqa
|
||||
logger.settings.level = 6
|
||||
|
||||
logger = Logger(2)
|
||||
with self.assertRaises(ValueError, msg='A settings instance changed level above limit after creation'): # noqa
|
||||
logger.settings.level = 0
|
||||
|
||||
def test_instance_counter(self) -> None:
|
||||
Config._INSTANCE = 0
|
||||
l1 = Logger()
|
||||
self.assertEqual(l1.settings._INSTANCE, 1, msg="First instance counter is wrong")
|
||||
l2 = Logger()
|
||||
self.assertEqual(l2.settings._INSTANCE, 2, msg="Second instance counter is wrong")
|
||||
|
||||
self.assertEqual(l1.settings._INSTANCE, 1, msg="Repeated instance counter is wrong")
|
||||
|
||||
def test_update_settings(self) -> None:
|
||||
self.logger.settings.update(success=4)
|
||||
self.assertEqual(self.logger.settings['success'], 4)
|
||||
|
||||
with self.assertRaises(TypeError, msg="self.logger object is directly assignable"):
|
||||
self.logger.settings['success'] = 4 # type: ignore
|
||||
|
||||
with self.assertRaises(ValueError, msg="not existing key passed into self.settings"):
|
||||
self.logger.settings.update(not_exists=4)
|
||||
|
||||
with self.assertRaises(TypeError, msg="String passed as value in self.settings"):
|
||||
self.logger.settings.update(success='4') # type: ignore
|
||||
|
||||
with self.assertRaises(ValueError, msg='0 Passed as valid key in self.settings'):
|
||||
self.logger.settings.update(success=0)
|
||||
|
||||
with self.assertRaises(ValueError, msg="Above the allowed limit passed as valid key in self.settings"): # noqa
|
||||
self.logger.settings.update(success=6)
|
||||
|
||||
with self.assertRaises(AttributeError, msg="self.settings is overwritable (`=`)"):
|
||||
self.logger.settings.settings = 'fsaf' # type: ignore
|
||||
|
||||
self.logger.settings.update(success=2, info=1)
|
||||
self.assertEqual(self.logger.settings['success'], 2, msg='Error in updating 2 values at once at `success`') # noqa
|
||||
self.assertEqual(self.logger.settings['info'], 1, msg='Error in updating 2 values at once at `info`') # noqa
|
||||
|
||||
def test_get_settings(self) -> None:
|
||||
with self.assertRaises(AttributeError, msg="self.settings is overwritable (`=`) instead of read-only"): # noqa
|
||||
self.logger.settings = 'fsaf' # type: ignore
|
||||
|
||||
def test_get_setting(self) -> None:
|
||||
self.assertEqual(self.logger.settings.get('info'), 3, msg="settings.get() returns value from wrong key or the value has changed") # noqa
|
||||
with self.assertRaises(KeyError):
|
||||
self.logger.settings.get('not_existing')
|
||||
|
||||
@unittest.mock.patch('sys.stdout', new_callable=io.StringIO)
|
||||
def test_success_msg(self, mock_stdout: Any) -> None:
|
||||
msg = "anythong"
|
||||
|
||||
self.logger.settings.level = 5
|
||||
self.logger.success(msg)
|
||||
self.assertFalse(mock_stdout.getvalue())
|
||||
|
||||
self.logger.info(msg)
|
||||
self.assertTrue(mock_stdout.getvalue())
|
||||
|
||||
self.logger.settings.level = 1
|
||||
self.logger.success(msg)
|
||||
self.assertTrue(mock_stdout.getvalue())
|
||||
|
||||
self.logger.info(msg)
|
||||
self.assertTrue(mock_stdout.getvalue())
|
||||
|
||||
def test_iter_next(self) -> None:
|
||||
for i2 in self.logger.settings: ...
|
||||
|
||||
for i1 in self.logger.settings: ...
|
||||
|
||||
self.assertEqual(i1, i2, msg="There is something wrong with Config.__iter__ and Config.__next__. Maybe the index (self._iter) is not refreshing correctly") # noqa
|
||||
|
||||
def test_log_to_file(self) -> None:
|
||||
msg = 'This should be written in the file'
|
||||
file = Path(f"{BASE_DIR}tests.txt")
|
||||
logger = Logger(1, str(file))
|
||||
|
||||
# Redirect the annoying log to '/dev/null'
|
||||
# (or according file for other platforms) and bring it back
|
||||
std_out = sys.stdout
|
||||
f = open(os.devnull, mode='w')
|
||||
sys.stdout = f
|
||||
logger.info(msg)
|
||||
f.close()
|
||||
sys.stdout = std_out
|
||||
|
||||
self.assertTrue(os.path.exists(file))
|
||||
with open(file, mode='r') as f:
|
||||
self.assertEqual(f"[INFO]: {msg}", f.read()[:-1]) # Slice to remove '\n' from the file
|
||||
os.remove(file)
|
||||
Reference in New Issue
Block a user