|
2 | 2 | from functools import wraps |
3 | 3 | from typing import Any, Callable, Literal, Optional, Tuple, TypeAlias |
4 | 4 |
|
5 | | -LOG_LEVEL: TypeAlias = Literal['CRITICAL','ERROR','WARNING','INFO','DEBUG','NOTSET','DEFAULT'] |
| 5 | +LOG_LEVEL: TypeAlias = Literal['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'NOTSET', 'DEFAULT'] |
6 | 6 |
|
7 | 7 | def logwrap( |
8 | | - before: Optional[Tuple[LOG_LEVEL, str]] | str = None, |
9 | | - after: Optional[Tuple[LOG_LEVEL, str]] | str = None, |
10 | | - show_args: Optional[LOG_LEVEL] = None |
11 | | - ) -> Callable: |
| 8 | + before: Optional[Tuple[LOG_LEVEL, str]] | str | bool = None, |
| 9 | + on_exception: Optional[Tuple[LOG_LEVEL, str]] | str | bool = None, |
| 10 | + after: Optional[Tuple[LOG_LEVEL, str]] | str | bool = None, |
| 11 | + ) -> Callable: |
12 | 12 | """ |
13 | 13 | Simple dynamic decorator to log function calls. Uses the logging module with your current project configurations. |
14 | | - Use LOG_LEVEL literal for using standard log levels. |
15 | | - **Warning**: Providing wrong log level won't raise any exception but will default to specified defaults with DEFAULT level. |
| 14 | + Use LOG_LEVEL literal for using standard log levels. |
| 15 | + The messages will get formated in proccess so you can use templating for better logging. |
| 16 | +
|
| 17 | + (`func`: Function Name, `args`: Arguments Tuple, `kwargs`: Keyword Arguments Dict, `e`: The Exception if Exists) |
16 | 18 |
|
| 19 | + Passing bool will use default levels and messages.( |
| 20 | + Before: 'DEBUG - Calling {func} - args:{args}, kwargs:{kwargs}' |
| 21 | + After: 'INFO - Function {func} ended' |
| 22 | + On Exception: 'ERROR - Error on {func}: {e}') |
| 23 | + |
| 24 | + **Warning**: If options are negative, it will not log the option. |
| 25 | + **Warning**: Providing wrong log level won't raise any exception but will default to specified defaults with DEFAULT level. |
| 26 | + |
17 | 27 | Args: |
18 | 28 | before: Tuple of log level and message to log before function call or string with default of `DEBUG` level. |
| 29 | + on_exception: Tuple of log level and message to log exception of function call or string with default of `ERROR` level. |
19 | 30 | after: Tuple of log level and message to log after function call or string with default of `INFO` level. |
20 | | - show_args: Log level to use for logging function parameters before a call (eg. Args and kwargs) defaults to `DEBUG` level. |
21 | 31 |
|
22 | 32 | Examples: |
23 | 33 | >>> # Example with Custom Levels |
24 | | - >>> @logwrap(before=('INFO', 'Function starting'), after=('INFO', 'Function ended'), show_args='DEBUG') |
| 34 | + >>> @logwrap(before=('INFO', '{func} starting, args={args} kwargs={kwargs}'), after=('INFO', '{my_func} ended')) |
25 | 35 | >>> def my_func(my_arg, my_kwarg=None): |
26 | 36 | >>> ... |
27 | 37 | >>> my_func('hello', my_kwarg=123) # calling the function |
28 | | - Info - My Function is Starting |
29 | | - Debug - Arguments: args=('hello',), kwargs={'my_kwarg': 123} |
30 | | - Info - Function Ended |
| 38 | + Info - my_func Starting, args=('hello',), kwargs={'my_kwarg': 123} |
| 39 | + Info - my_func Ended |
31 | 40 | >>> # Example with defaults. |
32 | | - >>> @logwrap(before='Starting', after='Ended') |
| 41 | + >>> @logwrap(before=True, after=True) |
33 | 42 | >>> def my_new_func(): |
34 | 43 | >>> ... |
35 | 44 | >>> my_new_func() |
36 | | - Debug - Starting |
37 | | - Info - Ended |
| 45 | + Debug - Calling my_new_func - args:(), kwargs:{} |
| 46 | + Info - Function my_new_func ended |
| 47 | + >>> # Example with Exception |
| 48 | + >>> @logwrap(on_exception=True) |
| 49 | + >>> def error_func(): |
| 50 | + >>> raise Exception('My exception msg') |
| 51 | + >>> error_func() |
| 52 | + Error - Error on error_func: My exception msg') |
38 | 53 | """ |
39 | | - if isinstance(before, str): |
40 | | - before = ('DEFAULT', before) |
| 54 | + def normalize( |
| 55 | + default_level: LOG_LEVEL, |
| 56 | + default_msg: str, |
| 57 | + option: Optional[Tuple[LOG_LEVEL, str]] | str | bool | None, |
| 58 | + ) -> Tuple[LOG_LEVEL, str] | None: |
| 59 | + """ |
| 60 | + Normalize the options to specified args and make the input to `Tuple[LOG_LEVEL, str] | None`. |
| 61 | + Returns None on negative inputs. |
| 62 | +
|
| 63 | + Args: |
| 64 | + default_level(LOG_LEVEL): default level on str, bool inputs |
| 65 | + default_msg(str): default msg on bool inputs |
| 66 | + option(Optional[Tuple[LOG_LEVEL, str]] | str | bool | None): The option to normalize |
| 67 | + |
| 68 | + Returns: |
| 69 | + (Tuple[LOG_LEVEL, str] | None): Normalized output for logging wraper |
| 70 | + """ |
| 71 | + if isinstance(option, bool) and option is True: |
| 72 | + return (default_level, default_msg) |
| 73 | + |
| 74 | + elif isinstance(option, str): |
| 75 | + return (default_level, option) |
41 | 76 |
|
42 | | - if isinstance(after, str): |
43 | | - after = ('DEFAULT', after) |
| 77 | + elif isinstance(option, tuple): |
| 78 | + return option |
44 | 79 |
|
45 | | - def decorator(func): |
| 80 | + elif not option or option is None: |
| 81 | + return None |
| 82 | + |
| 83 | + before = normalize('DEBUG', 'Calling {func} - args:{args}, kwargs:{kwargs}', before) |
| 84 | + after = normalize('INFO', 'Function {func} ended', after) |
| 85 | + on_exception = normalize('ERROR', 'Error on {func}: {e}', on_exception) |
| 86 | + |
| 87 | + def decorator(func: Callable) -> Callable: |
46 | 88 | @wraps(func) |
47 | | - def wrapper(*args, **kwargs): |
| 89 | + def wrapper(*args, **kwargs) -> Any: |
48 | 90 | logger = logging.getLogger(func.__module__) |
| 91 | + func_name = func.__name__ |
49 | 92 |
|
50 | | - # Log before |
51 | | - if before: |
52 | | - level, message = before |
53 | | - logger.log(getattr(logging, level, logging.DEBUG), message) |
| 93 | + fmt_context = { |
| 94 | + 'func': func_name, |
| 95 | + 'args': args, |
| 96 | + 'kwargs': kwargs, |
| 97 | + } |
54 | 98 |
|
55 | | - # Log method parameters if requested |
56 | | - if show_args: |
57 | | - level = getattr(logging, show_args, logging.DEBUG) |
58 | | - logger.log(level, f"Arguments: args={args}, kwargs={kwargs}") |
| 99 | + if before: |
| 100 | + level, msg = before |
| 101 | + logger.log(getattr(logging, level, logging.DEBUG), msg.format(**fmt_context)) |
59 | 102 |
|
60 | | - result = func(*args, **kwargs) |
| 103 | + try: |
| 104 | + result = func(*args, **kwargs) |
| 105 | + except Exception as e: |
| 106 | + if on_exception: |
| 107 | + level, msg = on_exception |
| 108 | + fmt_context['e'] = e |
| 109 | + logger.log(getattr(logging, level, logging.ERROR), msg.format(**fmt_context)) |
| 110 | + raise e |
61 | 111 |
|
62 | | - # Log after |
63 | 112 | if after: |
64 | | - level, message = after |
65 | | - logger.log(getattr(logging, level, logging.INFO), message) |
| 113 | + level, msg = after |
| 114 | + logger.log(getattr(logging, level, logging.INFO), msg.format(**fmt_context)) |
66 | 115 |
|
67 | 116 | return result |
68 | 117 | return wrapper |
|
0 commit comments