pawlogger 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pawlogger-0.1.0/PKG-INFO +13 -0
- pawlogger-0.1.0/README.md +3 -0
- pawlogger-0.1.0/pyproject.toml +18 -0
- pawlogger-0.1.0/src/pawlogger/__init__.py +15 -0
- pawlogger-0.1.0/src/pawlogger/config.py +43 -0
- pawlogger-0.1.0/src/pawlogger/config_loguru.py +146 -0
- pawlogger-0.1.0/src/pawlogger/consts.py +8 -0
- pawlogger-0.1.0/src/pawlogger/loggingdecorators/__init__.py +12 -0
- pawlogger-0.1.0/src/pawlogger/loggingdecorators/consts_formats.py +61 -0
- pawlogger-0.1.0/src/pawlogger/loggingdecorators/decorators.py +188 -0
- pawlogger-0.1.0/src/pawlogger/loggingdecorators/legacy.py +254 -0
- pawlogger-0.1.0/src/pawlogger/tests/loggingdecorators/conftest.py +115 -0
- pawlogger-0.1.0/src/pawlogger/tests/loggingdecorators/test_on_call.py +89 -0
- pawlogger-0.1.0/src/pawlogger/tests/loggingdecorators/test_on_class.py +139 -0
- pawlogger-0.1.0/src/pawlogger/tests/loggingdecorators/test_on_init.py +121 -0
- pawlogger-0.1.0/src/pawlogger/tests/loggingdecorators/test_on_new.py +68 -0
- pawlogger-0.1.0/src/pawlogger/tests/pawlogger/test_l_config.py +63 -0
pawlogger-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: pawlogger
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: logging tools
|
|
5
|
+
Author: paw
|
|
6
|
+
Author-email: paw <pawrequest@users.noreply.github.com>
|
|
7
|
+
Requires-Dist: loguru
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# pawlogger
|
|
12
|
+
|
|
13
|
+
Describe your project here.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.8.22,<0.9.0"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
[project]
|
|
7
|
+
name = "pawlogger"
|
|
8
|
+
version = "0.1.0"
|
|
9
|
+
description = "logging tools"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "paw", email = "pawrequest@users.noreply.github.com" }
|
|
12
|
+
]
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
requires-python = ">= 3.8"
|
|
15
|
+
dependencies = [
|
|
16
|
+
'loguru',
|
|
17
|
+
]
|
|
18
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .config_loguru import get_loguru
|
|
2
|
+
from .config import get_logger
|
|
3
|
+
from .consts import ASCTIME_PATTERN, CONSOLE_FORMAT_STR, FILE_FORMAT_STR, get_format_str
|
|
4
|
+
# ASCTIME_PATTERN = r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}"
|
|
5
|
+
# CONSOLE_FORMAT_STR = '{levelname} - {module}:{lineno} - {message}'
|
|
6
|
+
# FILE_FORMAT_STR = '{levelname} - {asctime} - {module}:{lineno} - {message}'
|
|
7
|
+
#
|
|
8
|
+
#
|
|
9
|
+
# def get_format_str(match_regex=False, console_or_file='file'):
|
|
10
|
+
# ret_str = ''
|
|
11
|
+
# return ret_str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__all__ = ['get_logger', 'get_loguru', 'ASCTIME_PATTERN', 'CONSOLE_FORMAT_STR', 'FILE_FORMAT_STR',
|
|
15
|
+
'get_format_str']
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
logging configuration for builtin logger
|
|
3
|
+
"""
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from . import consts
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_logger(logger_name=None, log_file=None, level=logging.DEBUG):
|
|
11
|
+
"""
|
|
12
|
+
Configure logging
|
|
13
|
+
|
|
14
|
+
:param logger_name: name of logger
|
|
15
|
+
:param log_file: path to log file
|
|
16
|
+
:param level: logging level
|
|
17
|
+
:return: logger
|
|
18
|
+
"""
|
|
19
|
+
if logger_name is None:
|
|
20
|
+
frame = inspect.stack()[1]
|
|
21
|
+
module = inspect.getmodule(frame[0])
|
|
22
|
+
logger_name = module.__name__ if module else '__main__'
|
|
23
|
+
|
|
24
|
+
if log_file is None:
|
|
25
|
+
log_file = f'{logger_name}.log'
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(logger_name)
|
|
28
|
+
logger.setLevel(level)
|
|
29
|
+
|
|
30
|
+
file_handler = logging.FileHandler(log_file)
|
|
31
|
+
console_handler = logging.StreamHandler()
|
|
32
|
+
|
|
33
|
+
file_formatter = logging.Formatter(consts.FILE_FORMAT_STR, style='{')
|
|
34
|
+
console_formatter = logging.Formatter(consts.CONSOLE_FORMAT_STR, style='{')
|
|
35
|
+
|
|
36
|
+
file_handler.setFormatter(file_formatter)
|
|
37
|
+
console_handler.setFormatter(console_formatter)
|
|
38
|
+
|
|
39
|
+
logger.addHandler(file_handler)
|
|
40
|
+
logger.addHandler(console_handler)
|
|
41
|
+
logger.debug(f'Logger created: {logger.name}')
|
|
42
|
+
|
|
43
|
+
return logger
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
import loguru
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
functions for configuring loguru
|
|
13
|
+
"""
|
|
14
|
+
CAT_COLOR_DICT = {
|
|
15
|
+
'episode': 'cyan',
|
|
16
|
+
'reddit': 'green',
|
|
17
|
+
'backup': 'magenta',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_loguru(
|
|
22
|
+
level: str = 'INFO',
|
|
23
|
+
log_file : Path | None = None,
|
|
24
|
+
profile: Literal['local', 'remote', 'default'] = 'local',
|
|
25
|
+
color_dict: dict | None = None
|
|
26
|
+
) -> logger:
|
|
27
|
+
"""
|
|
28
|
+
Configure loguru logger
|
|
29
|
+
|
|
30
|
+
:param log_file: path to log file
|
|
31
|
+
:param profile: log profile to use
|
|
32
|
+
:param color_dict: dictionary of log-category to colour mappings
|
|
33
|
+
:return: logger
|
|
34
|
+
"""
|
|
35
|
+
if color_dict:
|
|
36
|
+
global CAT_COLOR_DICT
|
|
37
|
+
CAT_COLOR_DICT = color_dict
|
|
38
|
+
|
|
39
|
+
if profile == 'local':
|
|
40
|
+
logger.info('Using local log profile')
|
|
41
|
+
terminal_format = log_fmt_local_terminal
|
|
42
|
+
elif profile == 'remote':
|
|
43
|
+
logger.info('Using remote log profile')
|
|
44
|
+
terminal_format = log_fmt_server_terminal
|
|
45
|
+
else:
|
|
46
|
+
raise ValueError(f'Invalid profile: {profile}')
|
|
47
|
+
|
|
48
|
+
logger.remove()
|
|
49
|
+
|
|
50
|
+
lvl = level.upper()
|
|
51
|
+
if log_file:
|
|
52
|
+
logger.add(log_file, rotation='1 day', delay=True, encoding='utf8', level=lvl)
|
|
53
|
+
logger.add(sys.stderr, level=lvl, format=terminal_format)
|
|
54
|
+
|
|
55
|
+
return logger
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# def log_fmt_local_terminal(record) -> str:
|
|
59
|
+
# """
|
|
60
|
+
# Format for local logging
|
|
61
|
+
#
|
|
62
|
+
# :param record: log record
|
|
63
|
+
# :return: formatted log record
|
|
64
|
+
# """
|
|
65
|
+
# category = record['extra'].get('category', 'General')
|
|
66
|
+
# bot_colour = BOT_COLOR.get(category, 'white')
|
|
67
|
+
# category = f'{category:<9}'
|
|
68
|
+
# max_length = 100
|
|
69
|
+
# file_txt = f"{record['file'].path}:{record['line']}"
|
|
70
|
+
#
|
|
71
|
+
# if len(file_txt) > max_length:
|
|
72
|
+
# file_txt = file_txt[:max_length]
|
|
73
|
+
#
|
|
74
|
+
# # clickable link only works at start of line
|
|
75
|
+
# return f"{file_txt:<{max_length}} | <lvl>{record['level']: <7} | {coloured(category, bot_colour)} | {record['message']}</lvl>\n"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def log_fmt_local_terminal(record: loguru.Record) -> str:
|
|
79
|
+
file_txt = f"{record['file'].path}:{record['line']}"
|
|
80
|
+
|
|
81
|
+
category = record['extra'].get('category', 'General')
|
|
82
|
+
category_txt = f'{category.title():<9}'
|
|
83
|
+
|
|
84
|
+
color = CAT_COLOR_DICT.get(category.lower(), 'white')
|
|
85
|
+
category_txt = f'| {coloured(category_txt, color)}' if category_txt != 'General' else ''
|
|
86
|
+
lvltext = f'<lvl>{record['level']: <7}</lvl>'
|
|
87
|
+
msg_txt = f'<lvl>{record['message']}</lvl>'
|
|
88
|
+
msg_txt = msg_txt.replace('{', '{{').replace('}', '}}')
|
|
89
|
+
# msg_txt = f'{record['message']}'
|
|
90
|
+
return f'{lvltext} {category_txt} | {msg_txt} | {file_txt}\n'
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def coloured(msg: str, colour: str) -> str:
|
|
94
|
+
"""
|
|
95
|
+
Colour a message
|
|
96
|
+
|
|
97
|
+
:param msg: message to colour
|
|
98
|
+
:param colour: colour to use
|
|
99
|
+
:return: coloured message
|
|
100
|
+
"""
|
|
101
|
+
return f'<{colour}>{msg}</{colour}>'
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def log_fmt_server_terminal(record) -> str:
|
|
105
|
+
"""
|
|
106
|
+
Format for server-side logging
|
|
107
|
+
|
|
108
|
+
:param record: log record
|
|
109
|
+
:return: formatted log record
|
|
110
|
+
"""
|
|
111
|
+
category = record['extra'].get('category', 'General')
|
|
112
|
+
category = f'{category:<9}'
|
|
113
|
+
colour = CAT_COLOR_DICT.get(category, 'white')
|
|
114
|
+
|
|
115
|
+
file_line = f"{record['file']}:{record['line']}- {record['function']}()"
|
|
116
|
+
bot_says = f"<bold>{coloured(category, colour):<9} </bold> | {coloured(record['message'], colour)}"
|
|
117
|
+
|
|
118
|
+
return f"<lvl>{record['level']: <7} </lvl>| {bot_says} | {file_line}\n"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def logger_wraps(*, entries=True, exits=True, level='DEBUG') -> callable:
|
|
122
|
+
"""
|
|
123
|
+
Decorator to log function entry and exit
|
|
124
|
+
|
|
125
|
+
:param entries: log entry
|
|
126
|
+
:param exits: log exit
|
|
127
|
+
:param level: log level
|
|
128
|
+
:return: decorator
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def wrapper(func):
|
|
132
|
+
name = func.__name__
|
|
133
|
+
|
|
134
|
+
@functools.wraps(func)
|
|
135
|
+
def wrapped(*args, **kwargs):
|
|
136
|
+
logger_ = logger.opt(depth=1)
|
|
137
|
+
if entries:
|
|
138
|
+
logger_.log(level, f"Entering '{name}' (args={args}, kwargs={kwargs})")
|
|
139
|
+
result = func(*args, **kwargs)
|
|
140
|
+
if exits:
|
|
141
|
+
logger_.log(level, "Exiting '{}' (result={})", name, result)
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
return wrapped
|
|
145
|
+
|
|
146
|
+
return wrapper
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
ASCTIME_PATTERN = r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}"
|
|
2
|
+
CONSOLE_FORMAT_STR = '{levelname} - {module}:{lineno} - {message}'
|
|
3
|
+
FILE_FORMAT_STR = '{levelname} - {asctime} - {module}:{lineno} - {message}'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_format_str(match_regex=False, console_or_file='file'):
|
|
7
|
+
ret_str = ''
|
|
8
|
+
return ret_str
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# ruff: noqa: F401
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from .decorators import on_call, on_class
|
|
5
|
+
from .legacy import on_init, on_new, on_init_og, on_call_og
|
|
6
|
+
|
|
7
|
+
# if sys.version_info >= (3, 12):
|
|
8
|
+
# from pawlogger.future.on_new_dec_312 import on_new
|
|
9
|
+
# else:
|
|
10
|
+
# if sys.version_info < (3, 8):
|
|
11
|
+
# print("Unsupported Python version")
|
|
12
|
+
# from .decorators import on_new
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Callable, Union
|
|
4
|
+
|
|
5
|
+
DFLT_LOGGER_STR = "DEFAULT_LOGGER_STR"
|
|
6
|
+
DFLT_LOG_LEVEL = logging.DEBUG
|
|
7
|
+
LOGGER_CLASS = logging.getLoggerClass()
|
|
8
|
+
LOGGER_LIKE = Union[str, LOGGER_CLASS, Callable]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def call_msg(func_name, args=None, kwargs=None, logargs=True, logdefaults=False):
|
|
12
|
+
if all([args is None, kwargs is None, logargs]):
|
|
13
|
+
raise ValueError("Must provide either args or kwargs if logargs is True")
|
|
14
|
+
|
|
15
|
+
args, kwargs = args or [], kwargs or {}
|
|
16
|
+
|
|
17
|
+
arg_details = f": {args}" if args else ""
|
|
18
|
+
arg_msg = f"{len(args)} arg(s){arg_details}"
|
|
19
|
+
|
|
20
|
+
kwarg_details = f" : {kwargs}" if kwargs else ""
|
|
21
|
+
kwarg_msg = f"{len(kwargs)} kwarg(s){kwarg_details}"
|
|
22
|
+
|
|
23
|
+
content = f"calling {func_name} with {arg_msg} and {kwarg_msg}"
|
|
24
|
+
return content
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def class_msg2(cls_or_self, use_new=False, args=None, kwargs=None, logargs=True, logdefaults=False):
|
|
28
|
+
if all([args is None, kwargs is None, logargs]):
|
|
29
|
+
raise ValueError("Must provide either args or kwargs if logargs is True")
|
|
30
|
+
|
|
31
|
+
args, kwargs = args or [], kwargs or {}
|
|
32
|
+
|
|
33
|
+
arg_details = f": {args}" if args else ""
|
|
34
|
+
arg_msg = f"{len(args)} arg(s){arg_details}"
|
|
35
|
+
|
|
36
|
+
kwarg_details = f" : {kwargs}" if kwargs else ""
|
|
37
|
+
kwarg_msg = f"{len(kwargs)} kwarg(s){kwarg_details}"
|
|
38
|
+
|
|
39
|
+
method_type = "creating" if use_new else "initializing"
|
|
40
|
+
content = f"{method_type} {cls_or_self.__class__.__name__} with {arg_msg} and {kwarg_msg}"
|
|
41
|
+
return content
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_log_msg(obj, args: dict = None, logargs=True, use_new=False):
|
|
45
|
+
if args is None and logargs:
|
|
46
|
+
raise ValueError('if want to log args then provide some!')
|
|
47
|
+
args = {k: v for k, v in args.items() if k not in ['self', 'cls']} if args else {}
|
|
48
|
+
arg_details = ': ' + ', '.join([f"{k} = {v}" for k, v in args.items()]) if args else ""
|
|
49
|
+
arg_msg = f"{len(args)} arg(s){arg_details}"
|
|
50
|
+
|
|
51
|
+
if inspect.isfunction(obj) or inspect.ismethod(obj):
|
|
52
|
+
content = f"calling {obj.__name__} with {arg_msg}"
|
|
53
|
+
|
|
54
|
+
elif inspect.isclass(obj):
|
|
55
|
+
method_type = "new:" if use_new else "init:"
|
|
56
|
+
class_name = obj.__name__ if inspect.isclass(obj) else obj.__class__.__name__
|
|
57
|
+
content = f"{method_type} {class_name} with {arg_msg}"
|
|
58
|
+
else:
|
|
59
|
+
raise TypeError("Unsupported object type for logging")
|
|
60
|
+
|
|
61
|
+
return content
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import logging
|
|
3
|
+
from functools import wraps
|
|
4
|
+
|
|
5
|
+
from .consts_formats import DFLT_LOGGER_STR, LOGGER_CLASS, LOGGER_LIKE, \
|
|
6
|
+
build_log_msg
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# from src.pawlogger.legacy import log_object
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def on_call(logger: LOGGER_LIKE, level=logging.DEBUG, logargs=True,
|
|
13
|
+
logdefaults=False, msg: str = "",
|
|
14
|
+
depth=0):
|
|
15
|
+
"""
|
|
16
|
+
Decorate a function with a wrapper which logs the call at the specified level.
|
|
17
|
+
Increase depth by 1 for each level of decorator nesting.
|
|
18
|
+
"""
|
|
19
|
+
const_depth = 2
|
|
20
|
+
total_depth = const_depth + depth
|
|
21
|
+
|
|
22
|
+
def decorator(func):
|
|
23
|
+
|
|
24
|
+
if not callable(func):
|
|
25
|
+
raise TypeError(f"{func} does not appear to be callable.")
|
|
26
|
+
|
|
27
|
+
if getattr(func, "__name__") == "__repr__":
|
|
28
|
+
raise RuntimeError("Cannot apply to __repr__ as this will cause infinite recursion!")
|
|
29
|
+
|
|
30
|
+
@wraps(func)
|
|
31
|
+
def wrapper(*args, **kwargs):
|
|
32
|
+
_logger = _get_logger(func, logger)
|
|
33
|
+
logger.debug(f'logger {logger.name}')
|
|
34
|
+
|
|
35
|
+
if not isinstance(_logger, LOGGER_CLASS):
|
|
36
|
+
raise TypeError(
|
|
37
|
+
f"logger argument had unexpected type {type(_logger)}, expected {LOGGER_CLASS}")
|
|
38
|
+
|
|
39
|
+
result = func(*args, **kwargs)
|
|
40
|
+
if logargs:
|
|
41
|
+
log_agnostic(_logger, func, level, total_depth, logargs=logargs, args=args,
|
|
42
|
+
kwargs=kwargs, logdefaults=logdefaults)
|
|
43
|
+
else:
|
|
44
|
+
log_agnostic(_logger, func, level, logargs=False, depth=total_depth)
|
|
45
|
+
return result
|
|
46
|
+
|
|
47
|
+
return wrapper
|
|
48
|
+
|
|
49
|
+
return decorator
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
#
|
|
53
|
+
# def on_class(logger: LOGGER_LIKE = DFLT_LOGGER_STR,
|
|
54
|
+
# level=logging.DEBUG,
|
|
55
|
+
# logargs=True,
|
|
56
|
+
# logdefaults=False,
|
|
57
|
+
# depth=0,
|
|
58
|
+
# decorate_init=True,
|
|
59
|
+
# decorate_new=False):
|
|
60
|
+
# """
|
|
61
|
+
# Unified decorator for logging calls to a class's __init__ and/or __new__ methods.
|
|
62
|
+
# """
|
|
63
|
+
# const_depth = 2
|
|
64
|
+
# total_depth = const_depth + depth
|
|
65
|
+
#
|
|
66
|
+
# def decorator(cls):
|
|
67
|
+
# if not inspect.isclass(cls):
|
|
68
|
+
# raise TypeError("on_class decorator can only be applied to classes.")
|
|
69
|
+
#
|
|
70
|
+
# original_init = cls.__init__ if decorate_init and hasattr(cls, '__init__') else None
|
|
71
|
+
# original_new = cls.__new__ if decorate_new and hasattr(cls, '__new__') else None
|
|
72
|
+
#
|
|
73
|
+
# def wrap_function(original_function, method_name):
|
|
74
|
+
# @wraps(original_function)
|
|
75
|
+
# def wrapper(*args, **kwargs):
|
|
76
|
+
# _logger = _get_logger(cls, logger)
|
|
77
|
+
# if logargs:
|
|
78
|
+
# bound_arguments = get_bound(cls, use_new=use_new,
|
|
79
|
+
# logdefault=logdefaults, args=args, kwargs=kwargs)
|
|
80
|
+
# log_msg = build_log_msg(cls, args=bound_arguments.arguments,
|
|
81
|
+
# use_new=(method_name == 'new'))
|
|
82
|
+
# else:
|
|
83
|
+
# log_msg = build_log_msg(cls, logargs=False, use_new=(method_name == 'new'))
|
|
84
|
+
#
|
|
85
|
+
# _logger.log(level=level, msg=log_msg, stacklevel=total_depth)
|
|
86
|
+
# return original_function(*args, **kwargs)
|
|
87
|
+
#
|
|
88
|
+
# return wrapper
|
|
89
|
+
#
|
|
90
|
+
# if original_init:
|
|
91
|
+
# cls.__init__ = wrap_function(original_init, 'init')
|
|
92
|
+
# if original_new:
|
|
93
|
+
# cls.__new__ = wrap_function(original_new, 'new')
|
|
94
|
+
#
|
|
95
|
+
# return cls
|
|
96
|
+
#
|
|
97
|
+
# return decorator
|
|
98
|
+
#
|
|
99
|
+
def on_class[T](logger: LOGGER_LIKE = DFLT_LOGGER_STR,
|
|
100
|
+
level=logging.DEBUG,
|
|
101
|
+
logargs=True,
|
|
102
|
+
logdefaults=False,
|
|
103
|
+
depth=0,
|
|
104
|
+
decorate_init=True,
|
|
105
|
+
decorate_new=False) -> T:
|
|
106
|
+
"""
|
|
107
|
+
Decorator for logging calls to a class's __init__ and/or __new__ methods.
|
|
108
|
+
If decorate_init is True, replace the class __init__ method with a wrapped version.
|
|
109
|
+
If decorate_new is True, do the same for the __new__ method.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def decorator(cls):
|
|
113
|
+
if not inspect.isclass(cls):
|
|
114
|
+
raise TypeError("on_class decorator can only be applied to classes.")
|
|
115
|
+
|
|
116
|
+
if decorate_init and hasattr(cls, '__init__'):
|
|
117
|
+
original_init = cls.__init__
|
|
118
|
+
wrapped_init = on_call(logger, level, logargs, logdefaults, depth=depth + 1)(
|
|
119
|
+
original_init)
|
|
120
|
+
cls.__init__ = wrapped_init
|
|
121
|
+
|
|
122
|
+
if decorate_new and hasattr(cls, '__new__'):
|
|
123
|
+
original_new = cls.__new__
|
|
124
|
+
wrapped_new = on_call(logger, level, logargs, logdefaults, depth=depth + 1)(
|
|
125
|
+
original_new)
|
|
126
|
+
cls.__new__ = wrapped_new
|
|
127
|
+
|
|
128
|
+
return cls
|
|
129
|
+
|
|
130
|
+
return decorator
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def get_bound(obj, use_new=False, logdefault=False, args=None, kwargs=None):
|
|
134
|
+
args = args or ()
|
|
135
|
+
kwargs = kwargs or {}
|
|
136
|
+
|
|
137
|
+
if inspect.isclass(obj):
|
|
138
|
+
init_or_new = obj.__new__ if use_new else obj.__init__
|
|
139
|
+
signature_ = inspect.signature(init_or_new)
|
|
140
|
+
args = obj, *args
|
|
141
|
+
elif callable(obj):
|
|
142
|
+
signature_ = inspect.signature(obj)
|
|
143
|
+
else:
|
|
144
|
+
raise TypeError()
|
|
145
|
+
|
|
146
|
+
bound_arguments = signature_.bind(*args, **kwargs)
|
|
147
|
+
if logdefault:
|
|
148
|
+
bound_arguments.apply_defaults()
|
|
149
|
+
|
|
150
|
+
return bound_arguments
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def log_agnostic(_logger, obj, level, depth, args=None, kwargs=None, logargs=True,
|
|
154
|
+
logdefaults=False,
|
|
155
|
+
use_new=False):
|
|
156
|
+
if logargs and args is None:
|
|
157
|
+
raise ValueError("if logargs then provide them")
|
|
158
|
+
args, kwargs = args or (), kwargs or {}
|
|
159
|
+
if logargs:
|
|
160
|
+
bound_arguments = get_bound(obj=obj, use_new=use_new, logdefault=logdefaults, args=args,
|
|
161
|
+
kwargs=kwargs)
|
|
162
|
+
log_msg = build_log_msg(obj, args=bound_arguments.arguments, use_new=use_new)
|
|
163
|
+
else:
|
|
164
|
+
log_msg = build_log_msg(obj, logargs=False, use_new=use_new)
|
|
165
|
+
|
|
166
|
+
_logger.log(level=level, msg=log_msg, stacklevel=depth)
|
|
167
|
+
...
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _get_logger(objec, loggerlike: LOGGER_LIKE):
|
|
171
|
+
if isinstance(loggerlike, LOGGER_CLASS):
|
|
172
|
+
_logger = loggerlike
|
|
173
|
+
|
|
174
|
+
elif isinstance(loggerlike, str):
|
|
175
|
+
_logger = getattr(objec, loggerlike, None)
|
|
176
|
+
_logger = _logger or logging.getLogger(loggerlike)
|
|
177
|
+
|
|
178
|
+
elif callable(loggerlike):
|
|
179
|
+
_logger = loggerlike()
|
|
180
|
+
|
|
181
|
+
else:
|
|
182
|
+
raise TypeError(
|
|
183
|
+
f"logger argument had unexpected type {type(loggerlike)}, expected {LOGGER_CLASS}")
|
|
184
|
+
|
|
185
|
+
if not isinstance(_logger, LOGGER_CLASS):
|
|
186
|
+
raise ValueError(f'Unable to get logger {loggerlike}')
|
|
187
|
+
|
|
188
|
+
return _logger
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import logging
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from typing import Callable, Union
|
|
5
|
+
|
|
6
|
+
from .consts_formats import DFLT_LOGGER_STR, LOGGER_CLASS, LOGGER_LIKE
|
|
7
|
+
from .decorators import _get_logger, log_agnostic
|
|
8
|
+
|
|
9
|
+
loggerClass = logging.getLoggerClass()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def on_call_og(logger: Union[loggerClass, Callable], level=logging.DEBUG, logargs=True,
|
|
13
|
+
msg: str = "", depth=0):
|
|
14
|
+
"""
|
|
15
|
+
When applied to a function, decorate it with a wrapper which logs the call using the given logger at the specified
|
|
16
|
+
level.
|
|
17
|
+
|
|
18
|
+
The "logger" argument must be an instance of a logger from the logging library, or a function which returns an
|
|
19
|
+
instance of a logger.
|
|
20
|
+
|
|
21
|
+
If logargs is True, log the function arguments, one per line.
|
|
22
|
+
|
|
23
|
+
If the decorated function is to be nested inside other decorators, increase the depth argument by 1 for each
|
|
24
|
+
additional level of nesting in order for the messages emitted to contain the correct source file name & line number.
|
|
25
|
+
"""
|
|
26
|
+
const_depth = 2
|
|
27
|
+
total_depth = const_depth + depth
|
|
28
|
+
|
|
29
|
+
def decorator(func):
|
|
30
|
+
|
|
31
|
+
if not callable(func):
|
|
32
|
+
raise TypeError(f"{func} does not appear to be callable.")
|
|
33
|
+
|
|
34
|
+
if getattr(func, "__name__") == "__repr__":
|
|
35
|
+
raise RuntimeError("Cannot apply to __repr__ as this will cause infinite recursion!")
|
|
36
|
+
|
|
37
|
+
@wraps(func)
|
|
38
|
+
def wrapper(*args, **kwargs):
|
|
39
|
+
|
|
40
|
+
_logger = logger() if inspect.isfunction(logger) else logger
|
|
41
|
+
|
|
42
|
+
if not isinstance(_logger, loggerClass):
|
|
43
|
+
raise TypeError(
|
|
44
|
+
f"logger argument had unexpected type {type(_logger)}, expected {loggerClass}")
|
|
45
|
+
|
|
46
|
+
content = f"calling {func} with {len(args)} arg(s) and {len(kwargs)} kwarg(s) "
|
|
47
|
+
if msg:
|
|
48
|
+
content = f"{content} ({msg})"
|
|
49
|
+
_logger.log(level, content, stacklevel=total_depth)
|
|
50
|
+
if logargs:
|
|
51
|
+
for n, arg in enumerate(args):
|
|
52
|
+
_logger.log(level, f" - arg {n:>2}: {type(arg)} {arg}", stacklevel=total_depth)
|
|
53
|
+
for m, (key, item) in enumerate(kwargs.items()):
|
|
54
|
+
_logger.log(level, f" - kwarg {m:>2}: {type(item)} {key}={item}",
|
|
55
|
+
stacklevel=total_depth)
|
|
56
|
+
return func(*args, **kwargs)
|
|
57
|
+
|
|
58
|
+
return wrapper
|
|
59
|
+
|
|
60
|
+
return decorator
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def on_init_og(logger: Union[str, loggerClass, Callable] = "logger", level=logging.DEBUG,
|
|
64
|
+
logargs=True, depth=0):
|
|
65
|
+
"""
|
|
66
|
+
When applied to a class or an __init__ method, decorate it with a wrapper which logs the __init__ call using the
|
|
67
|
+
given logger at the specified level.
|
|
68
|
+
|
|
69
|
+
If "logger" is a string, look up an attribute of this name in the initialised object and use it to log the message.
|
|
70
|
+
If "logger" is a function, call it to obtain a reference to a logger instance.
|
|
71
|
+
Otherwise, assume "logger" is an instance of a logger from the logging library and use it to log the message.
|
|
72
|
+
|
|
73
|
+
If logargs is True, the message contains the arguments passed to __init__.
|
|
74
|
+
|
|
75
|
+
If the decorated class or __init__ method is to be nested inside other decorators, increase the depth argument by 1
|
|
76
|
+
for each additional level of nesting in order for the messages emitted to contain the correct source file name &
|
|
77
|
+
line number.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
const_depth = 2
|
|
81
|
+
total_depth = const_depth + depth
|
|
82
|
+
|
|
83
|
+
def decorator(constructor):
|
|
84
|
+
|
|
85
|
+
if not callable(constructor):
|
|
86
|
+
raise TypeError(f"{constructor} does not appear to be callable.")
|
|
87
|
+
|
|
88
|
+
is_class = inspect.isclass(constructor)
|
|
89
|
+
|
|
90
|
+
to_call = getattr(constructor, "__init__") if is_class else constructor
|
|
91
|
+
|
|
92
|
+
@wraps(constructor)
|
|
93
|
+
def init_wrapper(self, *args, **kwargs):
|
|
94
|
+
|
|
95
|
+
_logger = getattr(self, logger) if isinstance(logger, str) \
|
|
96
|
+
else logger() if inspect.isfunction(logger) \
|
|
97
|
+
else logger
|
|
98
|
+
|
|
99
|
+
if not isinstance(_logger, loggerClass):
|
|
100
|
+
raise TypeError(
|
|
101
|
+
f"logger argument had unexpected type {type(_logger)}, expected {loggerClass}")
|
|
102
|
+
|
|
103
|
+
if logargs:
|
|
104
|
+
_logger.log(level, f"init: {self.__class__.__name__}({args=}, {kwargs=})",
|
|
105
|
+
stacklevel=total_depth)
|
|
106
|
+
else:
|
|
107
|
+
_logger.log(level, f"init: {self.__class__.__name__}()", stacklevel=total_depth)
|
|
108
|
+
|
|
109
|
+
to_call(self, *args, **kwargs)
|
|
110
|
+
|
|
111
|
+
if is_class:
|
|
112
|
+
setattr(constructor, "__init__", init_wrapper)
|
|
113
|
+
return constructor
|
|
114
|
+
else:
|
|
115
|
+
return init_wrapper
|
|
116
|
+
|
|
117
|
+
return decorator
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def on_init[T](logger: LOGGER_LIKE = DFLT_LOGGER_STR,
|
|
121
|
+
level=logging.DEBUG,
|
|
122
|
+
logargs=True,
|
|
123
|
+
logdefaults=False,
|
|
124
|
+
use_new=False,
|
|
125
|
+
depth=0
|
|
126
|
+
) -> [T]:
|
|
127
|
+
"""
|
|
128
|
+
Decorator for logging initialization calls to a class's __init__ method.
|
|
129
|
+
"""
|
|
130
|
+
const_depth = 2
|
|
131
|
+
total_depth = const_depth + depth
|
|
132
|
+
|
|
133
|
+
def decorator(constructor):
|
|
134
|
+
if inspect.isclass(constructor):
|
|
135
|
+
original_thing = constructor.__init__
|
|
136
|
+
else:
|
|
137
|
+
original_thing = constructor
|
|
138
|
+
|
|
139
|
+
@wraps(original_thing)
|
|
140
|
+
def wrapper(self, *args, **kwargs):
|
|
141
|
+
_logger = _get_logger(self, logger)
|
|
142
|
+
classname = self.__class__.__name__
|
|
143
|
+
result = original_thing(self, *args, **kwargs)
|
|
144
|
+
if logargs:
|
|
145
|
+
log_agnostic(_logger, args, kwargs, self, logdefaults, level, total_depth)
|
|
146
|
+
else:
|
|
147
|
+
log_agnostic(_logger, obj=self, logdefaults=logdefaults, logargs=False,
|
|
148
|
+
use_new=use_new)
|
|
149
|
+
# log_object(_logger, classname, level, total_depth, 'init')
|
|
150
|
+
return result
|
|
151
|
+
|
|
152
|
+
if inspect.isclass(constructor):
|
|
153
|
+
constructor.__init__ = wrapper
|
|
154
|
+
else:
|
|
155
|
+
constructor = wrapper
|
|
156
|
+
|
|
157
|
+
return constructor
|
|
158
|
+
|
|
159
|
+
return decorator
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def on_new(logger: LOGGER_LIKE = DFLT_LOGGER_STR,
|
|
163
|
+
level=logging.DEBUG,
|
|
164
|
+
logargs=True,
|
|
165
|
+
logdefaults=False,
|
|
166
|
+
depth=0):
|
|
167
|
+
"""
|
|
168
|
+
Decorator for logging calls to a class's __new__ method.
|
|
169
|
+
"""
|
|
170
|
+
const_depth = 2
|
|
171
|
+
total_depth = const_depth + depth
|
|
172
|
+
|
|
173
|
+
def decorator(constructor):
|
|
174
|
+
if inspect.isclass(constructor):
|
|
175
|
+
original_thing = constructor.__new__
|
|
176
|
+
else:
|
|
177
|
+
original_thing = constructor
|
|
178
|
+
|
|
179
|
+
@wraps(original_thing)
|
|
180
|
+
def wrapper(cls, *args, **kwargs):
|
|
181
|
+
_logger = _get_logger(cls, logger)
|
|
182
|
+
classname = cls.__name__ if inspect.isclass(cls) else cls.__class__.__name__
|
|
183
|
+
if logargs:
|
|
184
|
+
log_agnostic(_logger, args, classname, kwargs, cls, original_thing, logdefaults,
|
|
185
|
+
level, total_depth)
|
|
186
|
+
else:
|
|
187
|
+
log_object(_logger, classname, level, total_depth, 'new')
|
|
188
|
+
return original_thing(cls, *args, **kwargs)
|
|
189
|
+
|
|
190
|
+
if inspect.isclass(constructor):
|
|
191
|
+
setattr(constructor, "__new__", wrapper)
|
|
192
|
+
else:
|
|
193
|
+
constructor = wrapper
|
|
194
|
+
|
|
195
|
+
return constructor
|
|
196
|
+
|
|
197
|
+
return decorator
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# def log_with_args_cl(_logger, args, kwargs, callable_func, logdefaults, level, depth):
|
|
201
|
+
# callable_name = callable_func.__name__
|
|
202
|
+
# bound_arguments = get_bound_args(args, kwargs, callable_func)
|
|
203
|
+
# formatted_args = format_bound_args(bound_arguments, logdefaults)
|
|
204
|
+
# log_object(_logger, callable_name, level, depth, formatted_args)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# if not inspect.isclass(obj) and hasattr(obj, '__init__'):
|
|
208
|
+
# callable_obj = obj.__init__
|
|
209
|
+
# elif hasattr(obj, '__call__'):
|
|
210
|
+
# callable_obj = obj.__call__
|
|
211
|
+
# else:
|
|
212
|
+
# callable_obj = obj
|
|
213
|
+
#
|
|
214
|
+
# signature_ = inspect.signature(callable_obj)
|
|
215
|
+
# bound_arguments = signature_.bind(*args, **kwargs)
|
|
216
|
+
# return bound_arguments
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# def binder_func(obj, use_new = False, *args, **kwargs):
|
|
220
|
+
# # args = args or ()
|
|
221
|
+
# # kwargs = kwargs or {}
|
|
222
|
+
# args = args
|
|
223
|
+
# if inspect.isclass(obj):
|
|
224
|
+
# # args = (obj, *args)
|
|
225
|
+
# signature_ = inspect.signature(obj.__new__) if use_new else inspect.signature(obj.__init__)
|
|
226
|
+
# else:
|
|
227
|
+
# signature_ = inspect.signature(obj)
|
|
228
|
+
# res = signature_.bind(*args, **kwargs)
|
|
229
|
+
# return res
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def get_bound_args(args, kwargs, cls_or_self, init_or_new):
|
|
233
|
+
init_signature = inspect.signature(init_or_new)
|
|
234
|
+
bound_arguments = init_signature.bind(cls_or_self, *args, **kwargs)
|
|
235
|
+
return bound_arguments
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def format_bound_args(bound_arguments, logdefaults):
|
|
239
|
+
if logdefaults:
|
|
240
|
+
bound_arguments.apply_defaults()
|
|
241
|
+
formatted_args = ', '.join(f"{k}={v.__class__.__name__ if k == 'self' or v == 'cls' else v}"
|
|
242
|
+
for k, v in bound_arguments.arguments.items())
|
|
243
|
+
return formatted_args
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def log_object_cl(_logger: LOGGER_CLASS, callable_name: str, level, depth, formatted_args=None):
|
|
247
|
+
formatted_args = formatted_args or ''
|
|
248
|
+
_logger.log(level, f"{callable_name}({formatted_args})", stacklevel=depth)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def log_object(_logger: LOGGER_CLASS, classname: str, level, depth, msg_prefix: str,
|
|
252
|
+
formatted_args=None):
|
|
253
|
+
formatted_args = formatted_args or ''
|
|
254
|
+
_logger.log(level, f"{msg_prefix}: {classname}({formatted_args})", stacklevel=depth)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from pawlogger import DFLT_LOG_LEVEL
|
|
7
|
+
|
|
8
|
+
ARG1 = "value 1 for test"
|
|
9
|
+
ARG2 = "value 2 for test"
|
|
10
|
+
DFLT_ARG1 = "default value 1"
|
|
11
|
+
DFLT_ARG2 = "default value 2"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def test_logger():
|
|
16
|
+
logger_name = "test_logger_" + str(uuid.uuid4())
|
|
17
|
+
logger = logging.getLogger(logger_name)
|
|
18
|
+
logger.setLevel(DFLT_LOG_LEVEL)
|
|
19
|
+
yield logger
|
|
20
|
+
logger.handlers.clear()
|
|
21
|
+
logger = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DummyClass:
|
|
25
|
+
logger_cls_attr = logging.getLogger('logger_cls_attr')
|
|
26
|
+
logger_cls_attr.setLevel(DFLT_LOG_LEVEL)
|
|
27
|
+
|
|
28
|
+
def __init__(self, arg1, arg2, arg3=DFLT_ARG1):
|
|
29
|
+
self.arg1 = arg1
|
|
30
|
+
self.arg2 = arg2
|
|
31
|
+
self.arg3 = arg3
|
|
32
|
+
self.logger_inst_attr = logging.getLogger('logger_attr')
|
|
33
|
+
self.logger_inst_attr.setLevel(DFLT_LOG_LEVEL)
|
|
34
|
+
|
|
35
|
+
# def __new__(cls, arg1, arg2, arg3=DFLT_ARG1):
|
|
36
|
+
# return super().__new__(cls)
|
|
37
|
+
|
|
38
|
+
def dummy_instance_method(self, arg1, arg2):
|
|
39
|
+
return self, arg1, arg2
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def dummy_static_method(arg1, arg2):
|
|
43
|
+
return arg1, arg2
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def dummy_class_method(cls, arg1, arg2):
|
|
47
|
+
return cls, arg1, arg2
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class DummyNewWithArgs(DummyClass):
|
|
51
|
+
def __new__(cls, arg1, arg2, arg3=DFLT_ARG1):
|
|
52
|
+
return super().__new__(cls)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# todo move these
|
|
56
|
+
INIT_MSG = f"init: {DummyClass.__name__}"
|
|
57
|
+
NEW_MSG = f"new: {DummyClass.__name__}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def dummy_func(arg1, arg2):
|
|
61
|
+
return arg1, arg2
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def dummy_func_noargs():
|
|
65
|
+
return "No args"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def dummy_func_kwargs(arg1, arg2, kwarg3, kwarg4_with_def=None):
|
|
69
|
+
return arg1, arg2, kwarg3, kwarg4_with_def
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@pytest.fixture
|
|
73
|
+
def dummy_func_fxt():
|
|
74
|
+
return dummy_func
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@pytest.fixture
|
|
78
|
+
def dummy_func_noargs_fxt():
|
|
79
|
+
return dummy_func_noargs
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@pytest.fixture
|
|
83
|
+
def dummy_func_kwargs_fxt():
|
|
84
|
+
return dummy_func_kwargs
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@pytest.fixture
|
|
88
|
+
def dummy_class_fxt():
|
|
89
|
+
return DummyClass
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class DummyClassNoArgs:
|
|
93
|
+
def __init__(self):
|
|
94
|
+
self.value = "No args init"
|
|
95
|
+
|
|
96
|
+
def __new__(cls):
|
|
97
|
+
return super().__new__(cls)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class DummyClassDefaultArgs:
|
|
101
|
+
def __init__(self, arg1=DFLT_ARG1, arg2=DFLT_ARG2):
|
|
102
|
+
self.arg1 = arg1
|
|
103
|
+
self.arg2 = arg2
|
|
104
|
+
|
|
105
|
+
def __new__(cls, arg1=DFLT_ARG1, arg2=DFLT_ARG2):
|
|
106
|
+
return super().__new__(cls)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class DummyInheritedClass(DummyClass):
|
|
110
|
+
def __init__(self, arg1, arg2, extra_arg):
|
|
111
|
+
super().__init__(arg1, arg2)
|
|
112
|
+
self.extra_arg = extra_arg
|
|
113
|
+
|
|
114
|
+
def __new__(cls, arg1, arg2, extra_arg):
|
|
115
|
+
return super().__new__(cls)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from pawlogger import DFLT_LOGGER_STR, DFLT_LOG_LEVEL, build_log_msg
|
|
7
|
+
from pawlogger.loggingdecorators.decorators import on_call
|
|
8
|
+
from tests.loggingdecorators.conftest import ARG1, ARG2, dummy_func, dummy_func_kwargs, dummy_func_noargs
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.parametrize("logger_input", [
|
|
12
|
+
(DFLT_LOGGER_STR, DFLT_LOGGER_STR),
|
|
13
|
+
("test_logger_obj", logging.getLogger("test_logger_obj")),
|
|
14
|
+
("test_logger_callable", lambda: logging.getLogger("test_logger_callable")),
|
|
15
|
+
])
|
|
16
|
+
def test_on_call_with_various_loggers(caplog, logger_input):
|
|
17
|
+
logger_name, logger = logger_input
|
|
18
|
+
dummy = copy.copy(dummy_func)
|
|
19
|
+
decorated_test_function = on_call(logger=logger)(dummy)
|
|
20
|
+
argdict = {
|
|
21
|
+
'arg1': ARG1,
|
|
22
|
+
'arg2': ARG2,
|
|
23
|
+
}
|
|
24
|
+
with caplog.at_level(DFLT_LOG_LEVEL, logger=logger_name):
|
|
25
|
+
decorated_test_function(**argdict)
|
|
26
|
+
|
|
27
|
+
content = build_log_msg(dummy, args=argdict)
|
|
28
|
+
assert content in caplog.text
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Test functions with different argument types
|
|
33
|
+
@pytest.mark.parametrize("func, args, kwargs", [
|
|
34
|
+
(dummy_func, ('arg1', 'arg2'), {}),
|
|
35
|
+
(dummy_func_noargs, (), {}),
|
|
36
|
+
(dummy_func_kwargs, ('arg1', 'arg2'), {'kwarg3': 'value3', 'kwarg4_with_def': 'value4'}),
|
|
37
|
+
])
|
|
38
|
+
@pytest.mark.parametrize("logger_input", [
|
|
39
|
+
(DFLT_LOGGER_STR, DFLT_LOGGER_STR),
|
|
40
|
+
("test_logger_obj", logging.getLogger("test_logger_obj")),
|
|
41
|
+
("test_logger_callable", lambda: logging.getLogger("test_logger_callable")),
|
|
42
|
+
])
|
|
43
|
+
def test_on_call_functions(caplog, logger_input, func, args, kwargs):
|
|
44
|
+
logger_name, logger = logger_input
|
|
45
|
+
decorated_function = on_call(logger=logger)(func)
|
|
46
|
+
|
|
47
|
+
with caplog.at_level(DFLT_LOG_LEVEL, logger=logger_name):
|
|
48
|
+
result = decorated_function(*args, **kwargs)
|
|
49
|
+
|
|
50
|
+
# Build log message
|
|
51
|
+
args_dict = dict(zip(func.__code__.co_varnames, args))
|
|
52
|
+
args_dict.update(kwargs)
|
|
53
|
+
expected_log = build_log_msg(func, args=args_dict)
|
|
54
|
+
assert expected_log in caplog.text
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.mark.parametrize("method_name, args, kwargs", [
|
|
58
|
+
('dummy_instance_method', ('arg1', 'arg2'), {}),
|
|
59
|
+
('dummy_static_method', ('arg1', 'arg2'), {}),
|
|
60
|
+
('dummy_class_method', ('arg1', 'arg2'), {}),
|
|
61
|
+
])
|
|
62
|
+
@pytest.mark.parametrize("logger_input", [
|
|
63
|
+
(DFLT_LOGGER_STR, DFLT_LOGGER_STR),
|
|
64
|
+
("test_logger_obj", logging.getLogger("test_logger_obj")),
|
|
65
|
+
("test_logger_callable", lambda: logging.getLogger("test_logger_callable")),
|
|
66
|
+
])
|
|
67
|
+
def test_on_call_class_methods(caplog, logger_input, method_name, args, kwargs, dummy_class_fxt):
|
|
68
|
+
logger_name, logger = logger_input
|
|
69
|
+
dummy_class = dummy_class_fxt
|
|
70
|
+
method = getattr(dummy_class, method_name)
|
|
71
|
+
decorated_method = on_call(logger=logger)(method)
|
|
72
|
+
|
|
73
|
+
with caplog.at_level(DFLT_LOG_LEVEL, logger=logger_name):
|
|
74
|
+
if 'instance' in method_name:
|
|
75
|
+
# Create an instance of DummyClass for instance method test
|
|
76
|
+
instance = dummy_class(ARG1, ARG2)
|
|
77
|
+
result = decorated_method(instance, *args, **kwargs)
|
|
78
|
+
else:
|
|
79
|
+
result = decorated_method(*args, **kwargs)
|
|
80
|
+
|
|
81
|
+
# Build log message
|
|
82
|
+
full_args = (dummy_class,) + args if 'class' in method_name else args
|
|
83
|
+
full_args = (instance,) + args if 'instance' in method_name else full_args
|
|
84
|
+
args_dict = dict(zip(method.__code__.co_varnames[:len(full_args)], full_args))
|
|
85
|
+
args_dict.update(kwargs)
|
|
86
|
+
expected_log = build_log_msg(method, args=args_dict)
|
|
87
|
+
|
|
88
|
+
assert expected_log in caplog.text
|
|
89
|
+
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from pawlogger import DFLT_LOGGER_STR, build_log_msg
|
|
6
|
+
from pawlogger.loggingdecorators.decorators import on_class
|
|
7
|
+
from tests.loggingdecorators.conftest import (ARG1, ARG2, DFLT_LOG_LEVEL, DummyClass, DummyClassDefaultArgs,
|
|
8
|
+
DummyClassNoArgs,
|
|
9
|
+
DummyInheritedClass, DummyNewWithArgs)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.mark.parametrize("logger_input", [
|
|
13
|
+
(DFLT_LOGGER_STR, DFLT_LOGGER_STR),
|
|
14
|
+
("test_logger_obj", logging.getLogger("test_logger_obj")),
|
|
15
|
+
("test_logger_callable", lambda: logging.getLogger("test_logger_callable")),
|
|
16
|
+
])
|
|
17
|
+
def test_on_class_init(caplog, logger_input):
|
|
18
|
+
logger_name, logger = logger_input
|
|
19
|
+
DecoratedClass = on_class(logger=logger, decorate_init=True, decorate_new=False)(DummyClass)
|
|
20
|
+
|
|
21
|
+
with caplog.at_level(DFLT_LOG_LEVEL, logger=logger_name):
|
|
22
|
+
instance = DecoratedClass(ARG1, ARG2) # noqa 481
|
|
23
|
+
|
|
24
|
+
expected_log = f'calling __init__ with 2 arg(s): arg1 = {ARG1}, arg2 = {ARG2}'
|
|
25
|
+
assert expected_log in caplog.text
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.mark.parametrize("logger_input", [
|
|
29
|
+
(DFLT_LOGGER_STR, DFLT_LOGGER_STR),
|
|
30
|
+
("test_logger_obj", logging.getLogger("test_logger_obj")),
|
|
31
|
+
("test_logger_callable", lambda: logging.getLogger("test_logger_callable")),
|
|
32
|
+
])
|
|
33
|
+
def test_on_class_new(caplog, logger_input):
|
|
34
|
+
logger_name, logger = logger_input
|
|
35
|
+
DecoratedClass = on_class(logger=logger, decorate_init=False, decorate_new=True)(
|
|
36
|
+
DummyNewWithArgs)
|
|
37
|
+
|
|
38
|
+
with caplog.at_level(DFLT_LOG_LEVEL, logger=logger_name):
|
|
39
|
+
instance = DecoratedClass(ARG1, ARG2)
|
|
40
|
+
|
|
41
|
+
expected_log = f'calling __new__ with 2 arg(s): arg1 = {ARG1}, arg2 = {ARG2}'
|
|
42
|
+
assert expected_log in caplog.text
|
|
43
|
+
#
|
|
44
|
+
#
|
|
45
|
+
# @pytest.mark.parametrize("decorate_init, decorate_new", [
|
|
46
|
+
# (True, False),
|
|
47
|
+
# # (False, True),
|
|
48
|
+
# # (True, True)
|
|
49
|
+
# ])
|
|
50
|
+
# @pytest.mark.parametrize("class_type, init_args", [
|
|
51
|
+
# (DummyClassNoArgs, ()),
|
|
52
|
+
# # (DummyClassDefaultArgs, ()),
|
|
53
|
+
# # (DummyInheritedClass, (ARG1, ARG2, "Extra Arg")),
|
|
54
|
+
# ])
|
|
55
|
+
# @pytest.mark.parametrize("logger_input", [
|
|
56
|
+
# (DFLT_LOGGER_STR, DFLT_LOGGER_STR),
|
|
57
|
+
# # ("test_logger_obj", logging.getLogger("test_logger_obj")),
|
|
58
|
+
# # ("test_logger_callable", lambda: logging.getLogger("test_logger_callable")),
|
|
59
|
+
# ])
|
|
60
|
+
# def test_on_class_various_types(caplog, logger_input, class_type, init_args, decorate_init, decorate_new):
|
|
61
|
+
# logger_name, logger = logger_input
|
|
62
|
+
# DecoratedClass = on_class(logger=logger, decorate_init=decorate_init, decorate_new=decorate_new)(class_type)
|
|
63
|
+
#
|
|
64
|
+
# with caplog.at_level(DFLT_LOG_LEVEL, logger=logger_name):
|
|
65
|
+
# instance = DecoratedClass(*init_args)
|
|
66
|
+
#
|
|
67
|
+
# # args_dict = dict(zip(func.__code__.co_varnames, args))
|
|
68
|
+
# # args_dict.update(kwargs)
|
|
69
|
+
# # expected_log = build_log_msg(func, args=args_dict)
|
|
70
|
+
#
|
|
71
|
+
# method_name = "__init__" if decorate_init else "__new__"
|
|
72
|
+
# arg_details = ', '.join([f'arg{i+1} = {arg}' for i, arg in enumerate(init_args)])
|
|
73
|
+
# expected_log = f'calling {method_name} with {len(init_args)} arg(s): {arg_details}'
|
|
74
|
+
# assert expected_log in caplog.text
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@pytest.mark.parametrize("decorate_init, decorate_new", [
|
|
78
|
+
(True, False),
|
|
79
|
+
(False, True),
|
|
80
|
+
(True, True)
|
|
81
|
+
])
|
|
82
|
+
@pytest.mark.parametrize("class_type, init_args", [
|
|
83
|
+
(DummyClassNoArgs, ()),
|
|
84
|
+
(DummyClassDefaultArgs, ()),
|
|
85
|
+
(DummyInheritedClass, (ARG1, ARG2, "Extra Arg")),
|
|
86
|
+
])
|
|
87
|
+
@pytest.mark.parametrize("logger_input", [
|
|
88
|
+
(DFLT_LOGGER_STR, DFLT_LOGGER_STR),
|
|
89
|
+
("test_logger_obj", logging.getLogger("test_logger_obj")),
|
|
90
|
+
("test_logger_callable", lambda: logging.getLogger("test_logger_callable")),
|
|
91
|
+
])
|
|
92
|
+
|
|
93
|
+
def test_on_class_various_types(caplog, logger_input, class_type, init_args, decorate_init, decorate_new):
|
|
94
|
+
logger_name, logger = logger_input
|
|
95
|
+
DecoratedClass = on_class(logger=logger, decorate_init=decorate_init, decorate_new=decorate_new)(class_type)
|
|
96
|
+
|
|
97
|
+
with caplog.at_level(DFLT_LOG_LEVEL, logger=logger_name):
|
|
98
|
+
instance = DecoratedClass(*init_args)
|
|
99
|
+
|
|
100
|
+
# Determine the actual method and its class
|
|
101
|
+
method = DecoratedClass.__init__ if decorate_init else DecoratedClass.__new__
|
|
102
|
+
method_class = type(method)
|
|
103
|
+
|
|
104
|
+
# Construct args_dict based on method and class
|
|
105
|
+
arg_names = method.__code__.co_varnames[1:method.__code__.co_argcount]
|
|
106
|
+
args_dict = dict(zip(arg_names, init_args))
|
|
107
|
+
if decorate_init:
|
|
108
|
+
args_dict = {'self': instance, **args_dict}
|
|
109
|
+
else:
|
|
110
|
+
args_dict = {'cls': DecoratedClass, **args_dict}
|
|
111
|
+
|
|
112
|
+
expected_log = build_log_msg(method, args=args_dict)
|
|
113
|
+
|
|
114
|
+
assert expected_log in caplog.text
|
|
115
|
+
|
|
116
|
+
# def test_on_class_various_types(caplog, logger_input, class_type, init_args, decorate_init, decorate_new):
|
|
117
|
+
# logger_name, logger = logger_input
|
|
118
|
+
# DecoratedClass = on_class(logger=logger, decorate_init=decorate_init, decorate_new=decorate_new)(class_type)
|
|
119
|
+
#
|
|
120
|
+
# with caplog.at_level(DFLT_LOG_LEVEL, logger=logger_name):
|
|
121
|
+
# instance = DecoratedClass(*init_args)
|
|
122
|
+
#
|
|
123
|
+
# # Choose the method (__init__ or __new__) from the decorated class
|
|
124
|
+
# method = DecoratedClass.__init__ if decorate_init else DecoratedClass.__new__
|
|
125
|
+
# method_name = "__init__" if decorate_init else "__new__"
|
|
126
|
+
#
|
|
127
|
+
# # Extract argument names (excluding 'self' and 'cls')
|
|
128
|
+
# arg_names = method.__code__.co_varnames[1:method.__code__.co_argcount]
|
|
129
|
+
#
|
|
130
|
+
# # Construct the args_dict with correct argument names and values
|
|
131
|
+
# args_dict = dict(zip(arg_names, init_args))
|
|
132
|
+
# if decorate_init:
|
|
133
|
+
# args_dict = {'self': instance, **args_dict}
|
|
134
|
+
# else: # If decorate_new is True
|
|
135
|
+
# args_dict = {'cls': DecoratedClass, **args_dict}
|
|
136
|
+
#
|
|
137
|
+
# expected_log = build_log_msg(method, args=args_dict)
|
|
138
|
+
#
|
|
139
|
+
# assert expected_log in caplog.text
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from pawlogger import on_init
|
|
6
|
+
from pawlogger import DFLT_LOGGER_STR, DFLT_LOG_LEVEL
|
|
7
|
+
from tests.loggingdecorators.conftest import ARG1, ARG2, DFLT_ARG1, DummyClass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_on_init_dflt(caplog):
|
|
11
|
+
decorated_class = on_init()(DummyClass)
|
|
12
|
+
with caplog.at_level(logging.DEBUG, logger=DFLT_LOGGER_STR):
|
|
13
|
+
instance = decorated_class(ARG1, ARG2) # noqa: F841
|
|
14
|
+
msg = caplog.messages[-1]
|
|
15
|
+
assert INIT_MSG in msg
|
|
16
|
+
assert ARG1 in msg
|
|
17
|
+
assert ARG2 in msg
|
|
18
|
+
assert DFLT_ARG1 not in msg
|
|
19
|
+
caplog.clear()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.parametrize("logger_input", [
|
|
23
|
+
("default_logger", DFLT_LOGGER_STR),
|
|
24
|
+
("test_logger_instance", "test_logger"),
|
|
25
|
+
])
|
|
26
|
+
def test_on_init_with_various_loggers(caplog, test_logger, logger_input):
|
|
27
|
+
logger_type, logger = logger_input
|
|
28
|
+
logger = test_logger if logger == "test_logger" else logger
|
|
29
|
+
decorated_class = on_init(logger=logger)(DummyClass)
|
|
30
|
+
with caplog.at_level(logging.DEBUG,
|
|
31
|
+
logger=test_logger.name if logger_type == "test_logger_instance" else DFLT_LOGGER_STR):
|
|
32
|
+
instance = decorated_class(ARG1, arg2=ARG2) # noqa: F841
|
|
33
|
+
msg = caplog.messages[-1]
|
|
34
|
+
assert INIT_MSG in msg
|
|
35
|
+
assert ARG1 in msg
|
|
36
|
+
assert ARG2 in msg
|
|
37
|
+
assert DFLT_ARG1 not in msg
|
|
38
|
+
caplog.clear()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_on_init_with_exception(caplog, test_logger):
|
|
43
|
+
class DummyClassExcepts:
|
|
44
|
+
def __init__(self, arg1):
|
|
45
|
+
raise ValueError("Init error")
|
|
46
|
+
|
|
47
|
+
decorated_class = on_init(logger=test_logger)(DummyClassExcepts)
|
|
48
|
+
|
|
49
|
+
with caplog.at_level(logging.DEBUG, logger=test_logger.name):
|
|
50
|
+
with pytest.raises(ValueError):
|
|
51
|
+
instance = decorated_class(ARG1) # noqa: F841
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_on_init_with_depth(caplog, test_logger):
|
|
55
|
+
def dummy_decorator(func):
|
|
56
|
+
def wrapper(*args, **kwargs):
|
|
57
|
+
return func(*args, **kwargs)
|
|
58
|
+
|
|
59
|
+
return wrapper
|
|
60
|
+
|
|
61
|
+
@dummy_decorator
|
|
62
|
+
@on_init(logger=test_logger, depth=1)
|
|
63
|
+
class DummyClassDecorated(DummyClass):
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
with caplog.at_level(DFLT_LOG_LEVEL, logger=test_logger.name):
|
|
67
|
+
instance = DummyClassDecorated(ARG1, ARG2) # noqa: F841
|
|
68
|
+
msg = caplog.messages[-1]
|
|
69
|
+
|
|
70
|
+
assert INIT_MSG + 'Decorated' in msg
|
|
71
|
+
assert ARG1 in msg
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_on_init_with_callable_logger(caplog):
|
|
75
|
+
def logger_callable():
|
|
76
|
+
return logging.getLogger('callable_logger')
|
|
77
|
+
|
|
78
|
+
decorated_test_class = on_init(logger=logger_callable)(DummyClass)
|
|
79
|
+
with caplog.at_level(logging.DEBUG, logger='callable_logger'):
|
|
80
|
+
instance = decorated_test_class(ARG1, ARG2) # noqa: F841
|
|
81
|
+
|
|
82
|
+
msg = caplog.messages[-1]
|
|
83
|
+
|
|
84
|
+
assert INIT_MSG in msg
|
|
85
|
+
assert ARG1 in msg
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_on_init_with_class_attribute_logger(caplog, test_logger):
|
|
89
|
+
logger = DummyClass.logger_cls_attr
|
|
90
|
+
decorated_test_class = on_init(logger=logger.name)(DummyClass)
|
|
91
|
+
|
|
92
|
+
with caplog.at_level(DFLT_LOG_LEVEL, logger=logger.name):
|
|
93
|
+
instance = decorated_test_class(ARG1, ARG2) # noqa: F841
|
|
94
|
+
|
|
95
|
+
msg = caplog.messages[-1]
|
|
96
|
+
|
|
97
|
+
assert INIT_MSG in msg
|
|
98
|
+
assert ARG1 in msg
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_on_init_with_instance_attribute_logger(caplog, test_logger):
|
|
102
|
+
decorated_test_class = on_init(logger='logger_attr')(DummyClass)
|
|
103
|
+
with caplog.at_level(logging.DEBUG, logger='logger_attr'):
|
|
104
|
+
instance = decorated_test_class(ARG1, ARG2) # noqa: F841
|
|
105
|
+
|
|
106
|
+
msg = caplog.messages[-1]
|
|
107
|
+
assert INIT_MSG in msg
|
|
108
|
+
assert ARG1 in msg
|
|
109
|
+
assert ARG2 in msg
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_on_init_log_defaults(caplog, test_logger):
|
|
113
|
+
decorated_test_class = on_init(logger=test_logger, logdefaults=True)(DummyClass)
|
|
114
|
+
with caplog.at_level(logging.DEBUG, logger=test_logger.name):
|
|
115
|
+
instance = decorated_test_class(ARG1, ARG2) # noqa: F841
|
|
116
|
+
msg = caplog.messages[-1]
|
|
117
|
+
|
|
118
|
+
assert INIT_MSG in msg
|
|
119
|
+
assert ARG1 in msg
|
|
120
|
+
assert f"arg3={DFLT_ARG1}" in msg
|
|
121
|
+
caplog.clear()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# ruff: noqa: F841
|
|
2
|
+
import copy
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from pawlogger import on_new
|
|
8
|
+
from pawlogger import DFLT_LOGGER_STR, DFLT_LOG_LEVEL
|
|
9
|
+
from tests.loggingdecorators.conftest import ARG1, ARG2, DFLT_ARG1, DFLT_ARG2, DummyClass, NEW_MSG
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_with_logger_object(caplog, test_logger, dummy_class_fxt):
|
|
13
|
+
decorated_test_class = on_new(logger=test_logger)(DummyClass)
|
|
14
|
+
with caplog.at_level(logging.DEBUG, logger=test_logger.name):
|
|
15
|
+
instance = decorated_test_class(ARG1, arg2=ARG2) # noqa: F841
|
|
16
|
+
msg = caplog.records[0].msg
|
|
17
|
+
assert NEW_MSG in msg
|
|
18
|
+
caplog.clear()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_with_callable(caplog, test_logger):
|
|
22
|
+
def logger_callable():
|
|
23
|
+
return test_logger
|
|
24
|
+
|
|
25
|
+
decorated_test_class = on_new(logger=logger_callable)(DummyClass)
|
|
26
|
+
with caplog.at_level(logging.DEBUG, logger=test_logger.name):
|
|
27
|
+
instance = decorated_test_class(ARG1, ARG2) # noqa: F841
|
|
28
|
+
msg = caplog.records[0].msg
|
|
29
|
+
assert NEW_MSG in msg
|
|
30
|
+
caplog.clear()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_no_logargs(caplog, test_logger):
|
|
34
|
+
caplog.clear()
|
|
35
|
+
decorated_test_class = on_new(logger=test_logger, logargs=False)(DummyClass)
|
|
36
|
+
with caplog.at_level(DFLT_LOG_LEVEL, logger=test_logger.name):
|
|
37
|
+
instance = decorated_test_class(ARG1, ARG2) # noqa: F841
|
|
38
|
+
msg = caplog.records[0].msg
|
|
39
|
+
assert NEW_MSG in msg
|
|
40
|
+
assert ARG1 not in msg
|
|
41
|
+
assert ARG2 not in msg
|
|
42
|
+
assert DFLT_ARG1 not in msg
|
|
43
|
+
assert DFLT_ARG2 not in msg
|
|
44
|
+
caplog.clear()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_default_dec(caplog, test_logger):
|
|
48
|
+
dummy = copy.copy(DummyClass)
|
|
49
|
+
decorated_test_class = on_new()(dummy)
|
|
50
|
+
with caplog.at_level(logging.DEBUG, logger=DFLT_LOGGER_STR):
|
|
51
|
+
instance = decorated_test_class(ARG1, ARG2) # noqa: F841
|
|
52
|
+
msg = caplog.records[-1].msg
|
|
53
|
+
|
|
54
|
+
assert NEW_MSG in msg
|
|
55
|
+
assert ARG1 in msg
|
|
56
|
+
assert ARG2 in msg
|
|
57
|
+
assert DFLT_ARG1 not in msg
|
|
58
|
+
assert DFLT_ARG2 not in msg
|
|
59
|
+
caplog.clear()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_invalid_logger(caplog, test_logger):
|
|
63
|
+
dummy = copy.copy(DummyClass)
|
|
64
|
+
with pytest.raises(TypeError):
|
|
65
|
+
decorated_test_class = on_new(logger=123)(dummy)
|
|
66
|
+
with caplog.at_level(logging.DEBUG, logger=test_logger.name):
|
|
67
|
+
instance = decorated_test_class(ARG1) # noqa: F841
|
|
68
|
+
caplog.clear()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from pawlogger.consts import ASCTIME_PATTERN
|
|
8
|
+
from pawlogger import get_logger
|
|
9
|
+
|
|
10
|
+
test_params = [
|
|
11
|
+
(logging.DEBUG, 'Debug message', 'DEBUG', [42]),
|
|
12
|
+
(logging.INFO, 'Info message with number: {}', 'INFO', [42]),
|
|
13
|
+
(logging.WARNING, 'Warning message with float: {:.2f}', 'WARNING', [3.14159]),
|
|
14
|
+
(logging.ERROR, 'Error message with object: {}', 'ERROR', [Exception('Test error')]),
|
|
15
|
+
(logging.CRITICAL, 'Critical message with multiline\nNew line included', 'CRITICAL', (21,)),
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.mark.parametrize('log_level, log_message, level_name, format_args', test_params)
|
|
20
|
+
def test_logging(caplog, tmp_path: Path, log_level, log_message, level_name, format_args):
|
|
21
|
+
log_file = tmp_path / 'test.log'
|
|
22
|
+
logger = get_logger(str(log_file.name), level=log_level, log_file=log_file)
|
|
23
|
+
logger.log(log_level, log_message.format(*format_args))
|
|
24
|
+
|
|
25
|
+
formatted_message = log_message.format(*format_args)
|
|
26
|
+
assert formatted_message in caplog.messages[-1]
|
|
27
|
+
assert any(level_name in record.levelname for record in caplog.records)
|
|
28
|
+
|
|
29
|
+
assert os.path.exists(log_file)
|
|
30
|
+
with open(log_file) as file:
|
|
31
|
+
log_contents = file.read()
|
|
32
|
+
|
|
33
|
+
expected_pattern = re.compile(
|
|
34
|
+
f'{level_name} - {ASCTIME_PATTERN} - test_l_config:\\d{{2}} - {re.escape(formatted_message)}\n'
|
|
35
|
+
)
|
|
36
|
+
assert expected_pattern.search(log_contents)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def logger_from_factory(tmp_path: Path):
|
|
41
|
+
log_file = tmp_path / 'factory_logger.log'
|
|
42
|
+
return get_logger(log_file=str(log_file))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_logging_from_different_sources(caplog, tmp_path: Path, logger_from_factory):
|
|
46
|
+
# Logger from the test module
|
|
47
|
+
log_file_test = tmp_path / 'test_logger.log'
|
|
48
|
+
logger_test = get_logger(log_file=str(log_file_test))
|
|
49
|
+
|
|
50
|
+
# Log a message from the test module logger
|
|
51
|
+
test_message = 'Log message from test module'
|
|
52
|
+
logger_test.info(test_message)
|
|
53
|
+
|
|
54
|
+
# Log a message from the factory logger
|
|
55
|
+
factory_message = 'Log message from factory'
|
|
56
|
+
logger_from_factory.info(factory_message)
|
|
57
|
+
|
|
58
|
+
# Assertions
|
|
59
|
+
assert test_message in caplog.text
|
|
60
|
+
assert factory_message in caplog.text
|
|
61
|
+
assert os.path.exists(log_file_test) and os.path.exists(tmp_path / 'factory_logger.log')
|
|
62
|
+
|
|
63
|
+
# You can add more assertions to check the content of the log files
|