3

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()
alisianoi
  • 2,003
  • 3
  • 31
  • 46

3 Answers3

7

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?

N Chauhan
  • 3,407
  • 2
  • 7
  • 21
2

I was incredibly annoyed by this and eventually wrote a library to solve this problem: decopatch.

It supports two development styles: nested (like in python decorator factories) and flat (one less level of nesting). This is how your example would be implemented in flat mode:

from decopatch import function_decorator, DECORATED
from makefun import wraps

@function_decorator
def logged(disabled=False, logger=logging.getLogger('default'), func=DECORATED):

    # (1) create a signature-preserving wrapper
    @wraps(func)
    def _func_wrapper(*f_args, **f_kwargs):
        # stuff
        result = func(*f_args, **f_kwargs)
        # stuff
        return result

    # (2) return it
    return _func_wrapper

Note that I use makefun.wraps instead of functools.wraps here so that the signature is fully preserved (the wrapper is not called at all if the arguments are invalid).

decopatch supports an additional development style, that I call double-flat, that is dedicated to creating signature-preserving function wrappers like this one. Your example would be implemented like this:

from decopatch import function_decorator, WRAPPED, F_ARGS, F_KWARGS

@function_decorator
def logged(disabled=False, logger=logging.getLogger('default'), 
           func=WRAPPED, f_args=F_ARGS, f_kwargs=F_KWARGS):
    # this is directly the signature-preserving wrapper
    # stuff
    result = func(*f_args, **f_kwargs)
    # stuff
    return result

You can check that both styles work as expected:

@logged(disabled=True)
def foo():
    pass

@logged
def bar():
    pass

foo()

bar()

Please check the documentation for details.

smarie
  • 4,568
  • 24
  • 39
1

Or use partial (solution found in Python cookbook: 9.6)

from functools import wraps, partial

def foo(func=None, *, a=None, b=None):
    if func is None:
        return partial(foo, a=a, b=b)

    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper
Karolius
  • 543
  • 1
  • 6
  • 12