python-2.7pyinvoke

Using multiple decorators with Pyinvoke @task decorator


I am attempting to create a decorator, and use it alongside the pyinvoke @task decorator.

See:

def extract_config(func):
    print func

    def func_wrapper(cfg=None):
        config = read_invoke_config(cfg)
        return func(**config)

    return func_wrapper


@extract_config
@task
def zip_files(config):
    import zipfile
    ...

However, now from the command line when I execute

inv zip_files

I receive the output:

<Task 'zip_files'> No idea what 'zip_files' is!

No matter if @task comes before or after @extract_config, I lose the invoke task functionality and it doesn't recognize the function name..

What am I doing wrong here?


Solution

  • The syntax:

    @deco1
    @deco2
    def func(): pass
    

    Is pretty much equivalent to:

    def func(): pass
    func = deco1(deco2(func))
    

    So the decorator on the first line is applied after the decorator on the second line.

    The task decorator you are using from your library returns a Task object rather than another function, so your outer decorator may not be doing the right thing when replacing it in the global namespace with a wrapper function. To make it work, you'd need your wrapper to be an object that mimics all the relevant behavior of the Task object (I have no idea how easy or difficult that might be).

    A more straight forward approach may be to reverse the order of the decorators:

    @task
    @extract_config
    def zip_files(config):
    

    This is probably closer to working, but I suspect it still goes wrong for a simple reason. Your extract_config decorator returns a function with a different name than zip_files (it returns the function named wrapper, which hasn't made any effort to change its __name__ attribute), so the Task object doesn't know its name properly.

    To fix this, I'd suggest using functools.wraps in the decorator to copy the relevant attributes of the wrapped function into the wrapper function:

    def extract_config(func):
        print func
    
        @functools.wraps(func)
        def func_wrapper(cfg=None):
            config = read_invoke_config(cfg)
            return func(**config)
    
        return func_wrapper