Search code examples
pythonpython-3.xpython-decorators

creating decorator out of another decorator (python)


After spending several hours on the topic of decorators in python, I still have two issues.

First; if you have decorator without argument, sytntax is like this:

@decorator
def bye():
    return "bye"

which is just a syntactic sugar and is same as this

bye = decorator(bye)

but if I have a decorator with argument:

@decorator(*args)
def bye():
    return "bye"

How does "no-sugar" version looks like? Is the function passed inside as one of the arguments?

bye = decorator("argument", bye)

Second issue(which is related to the first, yet more practical example);

def permission_required(permission):
    def wrap(function):
        @functools.wraps(function)
            def wrapped_func(*args, **kwargs):
                if not current_user.can(permission):
                    abort(403)
                return function(*args, **kwargs)
            return wrapped_function
    return wrap

def admin_required(f):
    return permission_required(Permission.ADMINISTER)(f)

Here permission_required decorator is passed to a return statement of newly created decorator named admin_required. I have no idea how this works. Mainly the return statement where we returning original decorator + the function(in strange syntax). Can someone elaborate on this? - details are extremely welcome


Solution

  • When arguments are given in decorator notation,

    @decorator(a, b, c)
    def function(): pass
    

    it is syntactic sugar for writing

    def function(): pass
    
    function = decorator(a, b, c)(function)
    

    That is, decorator is called with arguments a, b, c, and then the object it returns is called with sole argument function.

    It is easiest to understand how that makes sense when the decorator is a class. I'm going to use your permission_required decorator for a running example. It could have been written thus:

    class permission_required:
        def __init__(self, permission):
            self.permission = permission
    
        def __call__(self, function):
            @functools.wraps(function)
            def wrapped_func(*args, **kwargs):
                if not current_user.can(self.permission):
                    abort(403)
                return function(*args, **kwargs)
            return wrapped_func
    
    admin_required = permission_required(Permission.ADMINISTER)
    

    When you use the decorator, e.g.

    @permission_required(Permission.DESTRUCTIVE)
    def erase_the_database():
        raise NotImplementedError # TBD: should we even have this?
    

    you instantiate the class first, passing Permission.DESTRUCTIVE to __init__, and then you call the instance as a function with erase_the_database as an argument, which invokes the __call__ method, which constructs the wrapped function and returns it.

    Thinking about it this way, admin_required should be easier to understand: it's an instance of the permission_required class, that hasn't yet been called. It's basically for shorthand:

    @admin_required
    def add_user(...): ...
    

    instead of typing out

    @permission_required(Permission.ADMINISTER)
    def add_user(...): ...
    

    Now, the way you had it...

    def permission_required(permission):
        def wrap(function):
            @functools.wraps(function)
                def wrapped_func(*args, **kwargs):
                    if not current_user.can(permission):
                        abort(403)
                    return function(*args, **kwargs)
                return wrapped_func
        return wrap
    

    is really just another way of writing the same thing. Returning wrap from permission_required implicitly creates a closure object. It can be called like a function, and when you do it calls wrap. It remembers the value of permission passed to permission_required so that wrap can use it. That's exactly what the class I showed above does. (In fact, compiled languages like C++ and Rust often implement closures by desugaring them into class definitions like the one I showed.)

    Notice that wrap itself does the same thing! We could expand it out even further...

    class permission_check_wrapper:
        def __init__(self, function, permission):
            self.function = function
            self.permission = permission
            functools.update_wrapper(self, function)
    
       def __call__(self, *args, **kwargs):
           if not current_user.can(self.permission):
               abort(403)
            return self.function(*args, **kwargs)
    
    class permission_required:
        def __init__(self, permission):
            self.permission = permission
    
        def __call__(self, function):
            return permission_check_wrapper(self.permission, function)
    

    Or we could do the entire job with functools.partial:

    def permission_check_wrapper(*args, function, permission, **kwargs):
       if not current_user.can(permission):
           abort(403)
        return function(*args, **kwargs)
    
    def wrap_fn_with_permission_check(function, *, permission):
        return functools.update_wrapper(
            functools.partial(permission_check_wrapper,
                              function=function,
                              permission=permission),
            wrapped=function)
    
    def permission_required(permission):
        return functools.partial(wrap_fn_with_permission_check,
                                 permission=permission)
    

    The beauty of defining @decorator(a,b,c) def foo to desugar to foo = decorator(a,b,c)(foo) is that the language doesn't care which of these several implementation techniques you pick.