Search code examples
pythonpython-3.xdecoratordocstring

Decorators with parameters


I have a collection of functions with (mostly) shared parameters but different processes. I'd like to use a decorator to add the description for each parameter to a function's headline-level docstring.

I've tried to mimic the structure found in this answer by incorporating a nested function within appender but failed. I've also tried functools.partial but something is slightly off.

My attempt:

def appender(func, *args):
    """Appends additional parameter descriptions to func's __doc__."""
    def _doc(func):
        params = ''.join([defaultdocs[arg] for arg in args])
        func.__doc__ += '\n' + params
        return func
    return _doc

defaultdocs = {

    'a' : 
    """
    a : int, default 0
        the first parameter
    """,

    'b' : 
    """
    b : int, default 1
        the second parameter
    """
    }

@appender('a')
def f(a):
    """Title-level docstring."""
    return a 

@appender('a', 'b')
def g(a, b):
    """Title-level docstring."""
    return a + b

This fails, and it fails I believe because the first arg passed to appender is interpreted as func. So when I view the resulting docstring for g I get:

print(g.__doc__)
Title-level docstring.

    b : int, default 1
        the second parameter

because, again, 'a' is interpreted to be 'func' when I want it to be the first element of *args. How can I correct this?

Desired result:

print(g.__doc__)
Title-level docstring.

    a : int, default 0
        the first parameter

    b : int, default 1
        the second parameter

Solution

  • This happens because the variable names you pass actually get captured into a func argument.

    In order to do callable decorators in Python you need to code the function twice, having external function to accept decorator arguments and internal function to accept original function. Callable decorators are just higher-order functions that return other decorators. For example:

    def appender(*args):  # This is called when a decorator is called,
                          # e. g. @appender('a', 'b')
        """Appends additional parameter descriptions to func's __doc__."""
        def _doc(func):  # This is called when the function is about
                         # to be decorated
            params = ''.join([defaultdocs[arg] for arg in args])
            func.__doc__ += '\n' + params
            return func
        return _doc
    

    The external (appender) function acts as a factory for new decorator while _doc function is an actual decorator. Always pass it this way:

    • Pass decorator args to the external function
    • Pass original function to the internal function

    Once the Python sees this:

    @appender('a', 'b')
    def foo(): pass
    

    ...it will do something like this under the hood:

    foo = appender('a', 'b')(foo)
    

    ...which expands to this:

    decorator = appender('a', 'b')
    foo = decorator(foo)
    

    Because of how scopes in Python work, each newly returned _doc function instance will have its own local args value from the external function.