1

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?

E_K
  • 2,159
  • 23
  • 39
  • What is `deco` doing? Can you show its implementation? – Willem Van Onsem Nov 29 '22 at 15:05
  • @Willem Van Onsem It just creates a database 'lock' to show that a command with this 'name' has been executed but basically the `deco` could be doing anything just that it needs to be run as a decorator – E_K Nov 29 '22 at 15:15
  • I would ask why does this even need to be a decorator? You are using classes, just make a mixin? – Abdul Aziz Barkat Nov 29 '22 at 15:19
  • Is the name you want supplied by the `--name` option that `add_arguments` adds to a parser? You might want to invert this a bit: have `--name` use a custom action that defines the class dynamically once you parse the arguments. – chepner Nov 29 '22 at 15:20
  • And what does `BaseCommand` provide? `add_arguments `doesn't seem to need to be a method at all, so unless `handle` makes use of something provided by `self` in some other part of the class, I would argue `handle` should just be a regular function as well. – chepner Nov 29 '22 at 15:28
  • 2
    @chepner `BaseCommand` is a class you inherit from Django to implement [custom management commands](https://docs.djangoproject.com/en/4.1/howto/custom-management-commands/) – Abdul Aziz Barkat Nov 29 '22 at 15:32
  • @AbdulAzizBarkat your approach seems like it would work just that I would have to make all other methods become classes. But I like it. Really clean. – E_K Nov 29 '22 at 15:36

3 Answers3

3

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
Abdul Aziz Barkat
  • 19,475
  • 3
  • 20
  • 33
2

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']
        # …
Willem Van Onsem
  • 443,496
  • 30
  • 428
  • 555
2

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.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343