Opened 11 years ago

Closed 11 years ago

#21513 closed Uncategorized (fixed)

method_decorator doesn't work with argumented Class-Based Decorator

Reported by: dpwrussell Owned by: nobody
Component: Utilities Version: 1.6
Severity: Normal Keywords: decorator, functools, CBV, method_decorator
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

https://code.djangoproject.com/ticket/13093 was a similar example of this. decorator_from_middleware has been adapted to use available_attrs. The same treatment is needed in method_decorator:

method_decorator blindly attempts to access attributes which are not present on an object, only on a class. This causes method_decorator to fail completely if it is used on any decorator used like this (with or without actual arguments):

class MyDecorator(object):
    def __init__(self, extra):
        self.extra = extra

    def __call__(self, f):

        def wrapped(a,b):
            return f(a,b) + self.extra
        return update_wrapper(wrapped, f)

class C(object):
    def __init__(self):
    @method_decorator(MyDecorator(5))
    def addStuff(self, a, b):
        return a+b

c = C()
print(c.addStuff(1,2)) # Should return 1+2+5=8

Something like this would be a fix, although I'm not really familiar enough with the nuances to know for 100% if this will work in all cases. Seems to be ok though:

from functools import update_wrapper
from django.utils.decorators import available_attrs, WRAPPER_ASSIGNMENTS


def method_decorator(decorator):
    """
    Converts a function decorator into a method decorator
    """
    # 'func' is a function at the time it is passed to _dec, but will eventually
    # be a method of the class it is defined it.
    def _dec(func):
        def _wrapper(self, *args, **kwargs):
            @decorator
            def bound_func(*args2, **kwargs2):
                return func(self, *args2, **kwargs2)
            # bound_func has the signature that 'decorator' expects i.e.  no
            # 'self' argument, but it is a closure over self so it can call
            # 'func' correctly.
            return bound_func(*args, **kwargs)
        # In case 'decorator' adds attributes to the function it decorates, we
        # want to copy those. We don't have access to bound_func in this scope,
        # but we can cheat by using it on a dummy function.

        @decorator
        def dummy(*args, **kwargs):
            pass
        update_wrapper(_wrapper, dummy)

        # Need to preserve any existing attributes of 'func', including the name.

        update_wrapper(_wrapper, func)

        return _wrapper

    update_wrapper(_dec, decorator, assigned=available_attrs(decorator))
    # Change the name to aid debugging.
    if hasattr(decorator, '__name__'):
        _dec.__name__ = 'method_decorator(%s)' % decorator.__name__
    else:
        _dec.__name__ = 'method_decorator(%s)' % decorator.__class__.__name__
    return _dec

Change History (3)

comment:1 by dpwrussell, 11 years ago

FYI It seems the https://code.djangoproject.com/ticket/13879 was going down a route of adding an method_decorator_with_args decorator and I wanted to differentiate from that.

comment:3 by dpwrussell <douglas.russell@…>, 11 years ago

Resolution: fixed
Status: newclosed

In 14940fdd96e77778cacdf0963913a343b1f33b9a:

Fixed #21513 -- Added method_decorator support for argumented decorator

Copied attributes into the decorated method and special case name
copy as this will not be present on a Class object. Added regression
test to decorator suite.

Note: See TracTickets for help on using tickets.
Back to Top