Is it possible to pass command line arguments to a decorator in Django?

Question:

I have a decorator that is supposed to use a parameter that’s passed in from the commandline e.g

@deco(name)
def handle(self, *_args, **options):
    name = options["name"]
def deco(name):
    // The name should come from commandline
    pass
class Command(BaseCommand):
    def add_arguments(self, parser):
        parser.add_argument(
            "--name",
            type=str,
            required=True,
        )
    @deco(//How can I pass the name here?)
    def handle(self, *_args, **options):
        name = options["name"]

any suggestions on this?

Asked By: E_K

||

Answers:

You can make a "meta-decorator", something like:

from functool import wraps

def metadeco(function):
    @wraps(function)
    def func(*args, **kwargs):
        name = kwargs['name']
        return deco(name)(function)(*args, **kwargs)
    return func

and then work with that meta-decorator:

class Command(BaseCommand):
    def add_arguments(self, parser):
        parser.add_argument(
            "--name",
            type=str,
            required=True,
        )
    
    @metadeco
    def handle(self, *_args, **options):
        name = options['name']
        # …
Answered By: Willem Van Onsem

You don’t have access to the command-line value when the @deco decorator is applied, no. But you can delay applying that decorator until you do have access.

Do so by creating your own decorator. A decorator is simply a function that applied when Python parses the @decorator and def functionname lines, right after Python created the function object; the return value of the decorator takes the place of the decorated function. What you need to make sure, then, is that your decorator returns a different function that can apply the deco decorator when the command is being executed.

Here is such a decorator:

from functools import wraps

def apply_deco_from_name(f):
    @wraps(f)
    def wrapper(self, *args, **options):
        # this code is called instead of the decorated method
        # and *now* we have access to the options mapping.
        name = options["name"]  # or use options.pop("name") to remove it
        decorated = deco(name)(f)  # the same thing as @deco(name) for the function
        return decorated(self, *args, **options)
        
    return wrapper

Then use that decorator on your command handler:

class Command(BaseCommand):
    def add_arguments(self, parser):
        parser.add_argument(
            "--name",
            type=str,
            required=True,
        )

    @apply_deco_from_name
    def handle(self, *_args, **options):
        name = options["name"]

What happens here? When Python handles the @apply_deco_from_name and def handle(...) lines, it sees this as a complete function statement inside the class. It’ll create a handle function object, then passes that to the decorator, so it calls apply_deco_from_name(handle). The decorator defined above return wrapper instead.

And when Django then executes the command handler, it will do so by calling that replacement with wrapper(command, [other arguments], name="command-line-value-for-name", [other options]). At that point the code creates a new decorated version of the handler with decorated = deco("command-line-value-for-name")(f) just like Python would have done had you used @deco("command-line-value-for-name") in your command class. deco("command-line-value-for-name") returns a decorator function, and deco("command-line-value-for-name")(f) returns a wrapper, and you can call that wrapper at the end.

Answered By: Martijn Pieters

Your decorator doesn’t really need to be a decorator. Since you are using classes, you can make use of the mixin pattern:

class YourMixin:
    def handle(self, name):
        # Code that was previously in deco


class Command(YourMixin, BaseCommand):
    def add_arguments(self, parser):
        parser.add_argument(
            "--name",
            type=str,
            required=True,
        )

    def handle(self, *_args, **options):
        # Code before calling YourMixin.handle
        name = options["name"]
        super().handle(name)
        # Code after calling YourMixin.handle
Answered By: Abdul Aziz Barkat
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.