Below is my @logged()
decorator maker. Here is roughly how it works:
logger
instance and a disabled
flag.disabled
is False
, it outputs some logs before/after the decorated function.disabled
is True
, it outputs nothing and also suppresses the logger
for the decorated function.Both the logger
and disabled
arguments have their default values. However, when I want to use the default values, I still have to write empty parenthesis, like so:
@logged()
def foo():
pass
Is there any way to get rid of these empty parenthesis when I just want the default arguments? Here is an example of what I would like to have:
@logged
def foo():
pass
@logged(disabled=True)
def bar():
pass
The code of the @logged()
decorator maker:
import logging
import logging.config
from functools import wraps
def logged(logger=logging.getLogger('default'), disabled=False):
'''
Create a configured decorator that controls logging output of a function
:param logger: the logger to send output to
:param disabled: True if the logger should be disabled, False otherwise
'''
def logged_decorator(foo):
'''
Decorate a function and surround its call with enter/leave logs
Produce logging output of the form:
> enter foo
...
> leave foo (returned value)
'''
@wraps(foo)
def wrapper(*args, **kwargs):
was_disabled = logger.disabled
# If the logger was not already disabled by something else, see if
# it should be disabled by us. Important effect: if foo uses the
# same logger, then any inner logging will be disabled as well.
if not was_disabled:
logger.disabled = disabled
logger.debug(f'enter {foo.__qualname__}')
result = foo(*args, **kwargs)
logger.debug(f'leave {foo.__qualname__} ({result})')
# Restore previous logger state:
logger.disabled = was_disabled
return result
return wrapper
return logged_decorator
logging.config.dictConfig({
'version': 1,
'formatters': {
'verbose': {
'format': '%(asctime)22s %(levelname)7s %(module)10s %(process)6d %(thread)15d %(message)s'
}
, 'simple': {
'format': '%(levelname)s %(message)s'
}
}
, 'handlers': {
'console': {
'level': 'DEBUG'
, 'class': 'logging.StreamHandler'
, 'formatter': 'verbose'
}
},
'loggers': {
'default': {
'handlers': ['console']
, 'level': 'DEBUG',
}
}
})
@logged()
def foo():
pass
if __name__ == '__main__':
foo()
You can make use of an if-else inside the decorator body:
def logged(func=None, *, disabled=False, logger=logging.default()):
def logged_decorator(func):
# stuff
def wrapper(*args_, **kwargs):
# stuff
result = func(*args_, **kwargs)
# stuff
return result
return wrapper
if func:
return logged_decorator(func)
else:
return logged_decorator
The (func=None, *, logger=..., disabled=False)
has an asterisk arg to denote the last 2 arguments as keyword-only arguments as any more arguments beside func are unpacked into the *
which had no identifier in this case so are effectively 'lost'. These means you must use keyword arguments when using the decorator normally:
@logged(
disabled=True,
logged=logging.logger # ...
)
def foo(): pass
Or...
@logged
def bar(): pass
See here: How to build a decorator with optional parameters?