Search code examples
pythonloggingpython-decorators

Decorator with arguments: avoid parenthesis when no arguments


Below is my @logged() decorator maker. Here is roughly how it works:

  1. It accepts a logger instance and a disabled flag.
  2. If disabled is False, it outputs some logs before/after the decorated function.
  3. If 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()

Solution

  • 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?