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.
@@ -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,3 @@
1
+ # pawlogger
2
+
3
+ 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