Decorating CBV methods with additional arguments

Hey,

So I have some shared extra processing that needs to happen on a bunch of CBV methods (such as saving related objects upon form_valid()).

So I have used the @method_decorator(decorator_name, name="target_method) to add that processing upon form_valid().

That works fine - however I did have to modify the target_method to accommodate this, as the decorator needs extra arguments.

For instance:

@method_decorator(save_related, name="form_valid")
class SomeCreateView(...):
   ...
    def form_valid(self, form, formset):
        self.object = form.save()
        formset.instance = self.object
        formset.save()
        # return HttpResponseRedirect(self.get_success_url())
        return {"httpresponse":HttpResponseRedirect(self.get_success_url()), "saved_instance": self.object, "mapping": self.related_field_map}

The decorator:

def save_related(func):
    @functools.wraps(func)
    def wrapper_test_decor(*args, **kwargs):
        """ input: args[0]: Form obj (form)
            args[1]: Formset obj (Form as well - but of the class of the inlines)

        """
        value = func(*args, **kwargs)
        obj = value["saved_instance"]
        mapping = value["mapping"]

        for src, dest in mapping:
            attr_chain = src.split(sep=".")             # since the value to fetch may span multiple foreign keys
            attr = attr_chain.pop(0)
            tmp_obj = getattr(obj, attr)
            while attr and tmp_obj:            # attr. will be a foreign Key model, until the last one where it will be the value to set to dest
                attr = attr_chain.pop(0) if len(attr_chain) > 0 else None
                tmp_obj = getattr(tmp_obj, attr) if attr else tmp_obj

            setattr(obj, dest, tmp_obj)

            # save the resulting object, after all related fields have been set
        obj.save()
        return value["httpresponse"]
    return wrapper_test_decor

So basically, the form_valid() code had to be modified so that it returns the extra arguments the decorator needs to do its job. Among those arguments is the HttpResponse() the form_valid() would normally returns. Then the decorators returns that after doing its extra processing.

With a regular decorator, I could simply do:

@mydec(extra_argument=foo)
def decorated_method(...)
   ...

def mydec(extra_argument):
   # extra stuff available here

However, with @method_decorator, I can’t seem to be able to find an equivalent way to pass extra arguments to the decorator. Of course my current setup works fine - I just don’t like the idea of having to modify the decorated method in order to accomodate the needs of the decorators, especially since those modifications ONLY have to do with passing arguments…

Any way to accomplish this better? I’m guessing there may be a better way to save related fields based on submitted form data. However more generally I’m still interested in passing extra arguments to decorators as I’ll be using them for other stuff (such as outputting pdfs).

I believe you should have access to self within the decorator. However, I don’t think that’d result in cleaner code. You could also change what you’re trying to decorate and write some logic that wraps form.save().

If it were me, I’d aim to create a new CBV mixin that would define related_field_map as a property, have a function that would do the save_related part and call that from within valid_form sidestepping the need for the decorator altogether.

Not sure this will work, but should help explain my idea.

class SaveRelatedMixin:
    related_field_map = None

    def save_related(self, instance):
        if self.related_field_map is None:
            raise ValueError("something here")

        # Save logic here

    def form_valid(self, form, formset):
        self.object = form.save()
        formset.instance = self.object
        formset.save()

        # Call mixin's special function
        self.save_related(self.object)

        # call to super or return HttpResponseRedirect(self.get_success_url())
2 Likes

I realized I didn’t answer your actual question,. You can’t pass extra args with django.utils.decorators.method_decorator. Check out the source code for it. You could create your own decorator that would pass along additional args and kwargs. In that same file there’s make_middleware_decorator and decorator_from_middleware that use that same premise.

Right, a mixin would also make sense there. Might actually be more django-ic too. It didn’t really occur to me as an option.

Actually, coming to think of it, in a lot of cases using a mixin might be an option whenever decorators are considered. Thanks for the idea.