views:

339

answers:

4

I have been trying to create a decorator that can be used with both functions and methods in python. This on it's own is not that hard, but when creating a decorator that takes arguments, it seems to be.

class methods(object):
    def __init__(self, *_methods):
        self.methods = _methods

    def __call__(self, func): 
        def inner(request, *args, **kwargs):
            print request
            return func(request, *args, **kwargs)
        return inner

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        new_func = self.func.__get__(obj, type)
        return self.__class__(new_func)

The above code wraps the function/method correctly, but in the case of a method, the request argument is the instance it is operating on, not the first non-self argument.

Is there a way to tell if the decorator is being applied to a function instead of a method, and deal accordingly?

+4  A: 

The decorator is always applied to a function object -- have the decorator print the type of its argument and you'll be able to confirm that; and it should generally return a function object, too (which is already a decorator with the proper __get__!-) although there are exceptions to the latter.

I.e, in the code:

class X(object):

  @deco
  def f(self): pass

deco(f) is called within the class body, and, while you're still there, f is a function, not an instance of a method type. (The method is manufactured and returned in f's __get__ when later f is accessed as an attribute of X or an instance thereof).

Maybe you can better explain one toy use you'd want for your decorator, so we can be of more help...?

Edit: this goes for decorators with arguments, too, i.e.

class X(object):

  @deco(23)
  def f(self): pass

then it's deco(23)(f) that's called in the class body, f is still a function object when passed as the argument to whatever callable deco(23) returns, and that callable should still return a function object (generally -- with exceptions;-).

Alex Martelli
+3  A: 

Since you're already defining a __get__ to use your decorator on the Bound Method, you could pass a flag telling it if it's being used on a method or function.

class methods(object):
    def __init__(self, *_methods, called_on_method=False):
        self.methods = _methods
        self.called_on_method

    def __call__(self, func):
        if self.called_on_method:
            def inner(self, request, *args, **kwargs):
                print request
                return func(request, *args, **kwargs)
        else:
            def inner(request, *args, **kwargs):
                print request
                return func(request, *args, **kwargs)
        return inner

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        new_func = self.func.__get__(obj, type)
        return self.__class__(new_func, called_on_method=True)
ironfroggy
A: 

A partial (specific) solution I have come up with relies on exception handling. I am attempting to create a decorator to only allow certain HttpRequest methods, but make it work with both functions that are views, and methods that are views.

So, this class will do what I want:

class methods(object):
    def __init__(self, *_methods):
        self.methods = _methods

    def __call__(self, func): 
        @wraps(func)
        def inner(*args, **kwargs):
            try:
                if args[0].method in self.methods:
                    return func(*args, **kwargs)
            except AttributeError:
                if args[1].method in self.methods:
                    return func(*args, **kwargs)
            return HttpResponseMethodNotAllowed(self.methods)
        return inner

Here are the two use cases: decorating a function:

@methods("GET")
def view_func(request, *args, **kwargs):
    pass

and decorating methods of a class:

class ViewContainer(object):
    # ...

    @methods("GET", "PUT")
    def object(self, request, pk, *args, **kwargs):
        # stuff that needs a reference to self...
        pass

Is there a better solution than to use exception handling?

Matthew Schinckel
+3  A: 

To expand on the __get__ approach. This can be generalized into a decorator decorator.

class _MethodDecoratorAdaptor(object):
    def __init__(self, decorator, func):
        self.decorator = decorator
        self.func = func
    def __call__(self, *args, **kwargs):
        return self.decorator(self.func)(*args, **kwargs)
    def __get__(self, instance, owner):
        return self.decorator(self.func.__get__(instance, owner))

def auto_adapt_to_methods(decorator):
    """Allows you to use the same decorator on methods and functions,
    hiding the self argument from the decorator."""
    def adapt(func):
        return _MethodDecoratorAdaptor(decorator, func)
    return adapt

In this way you can just make your decorator automatically adapt to the conditions it is used in.

def allowed(*allowed_methods):
    @auto_adapt_to_methods
    def wrapper(func):
        def wrapped(request):
            if request not in allowed_methods:
                raise ValueError("Invalid method %s" % request)
            return func(request)
        return wrapped
    return wrapper

Notice that the wrapper function is called on all function calls, so don't do anything expensive there.

Usage of the decorator:

class Foo(object):
    @allowed('GET', 'POST')
    def do(self, request):
        print "Request %s on %s" % (request, self)

@allowed('GET')
def do(request):
    print "Plain request %s" % request

Foo().do('GET')  # Works
Foo().do('POST') # Raises
Ants Aasma
You should add `update_wrapper(self, func)` to the *beginning* of `_MethodDecoratorAdaptor.__init__` (where update_wrapper is from functools module). This makes the resulting decorators preserve custom attributes on the functions/callables they decorate, while keeping them composable as well.
spookylukey
I've discovered that this method only works in some circumstances, and is extremely difficult to debug when it doesn't work. http://groups.google.com/group/django-developers/msg/f36976f5cfbcbeb3
spookylukey