3

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?

Alpenglow
  • 173
  • 1
  • 17
  • There's a bug raised for the issue: https://github.com/pyinvoke/invoke/issues/555, still open. – petre Sep 18 '20 at 16:23

2 Answers2

4

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
Blckknght
  • 100,903
  • 11
  • 120
  • 169
1

I couldn't get Blcknght's solution to work, but I found a workaround:

def decorator(fn):
    @functools.wraps(fn)
    def _decorated(ctx, *args, **kwargs):
        return fn(ctx, *args, **kwargs)
    return _decorated

@invoke.task
def some_task(ctx, config="Something"):
    @decorator
    def _inner(ctx):
        print(config)
        ...

    return _inner(ctx)

Wrapping the inner function prevents it from obscuring the underlying function, allowing invoke to recognise the decorator and allow it to inspect the arguments of the function.

Ewan Jones
  • 11
  • 2
  • Thanks for the solution! In this example, only adding the [`@functools.wraps(fn)`](https://docs.python.org/fr/3.9/library/functools.html#functools.wraps) decorator did the trick to my own decorator! – sodimel Jan 15 '21 at 09:44