Does python allow me to pass dynamic variables to a decorator at runtime?
Asked Answered
S

4

6

I am attempting to integrate a very old system and a newer system at work. The best I can do is to utilize an RSS firehouse type feed the system utilizes. The goal is to use this RSS feed to make the other system perform certain actions when certain people do things.

My idea is to wrap a decorator around certain functions to check if the user (a user ID provided in the RSS feed) has permissions in the new system.

My current solution has a lot of functions that look like this, which are called based on an action field in the feed:

actions_dict = {
    ...
    'action1': function1
}

actions_dict[RSSFEED['action_taken']](RSSFEED['user_id'])

def function1(user_id):
    if has_permissions(user_id):
         # Do this function

I want to create a has_permissions decorator that takes the user_id so that I can remove this redundant has_permissions check in each of my functions.

@has_permissions(user_id)
def function1():
    # Do this function

Unfortunately, I am not sure how to write such a decorator. All the tutorials I see have the @has_permissions() line with a hardcoded value, but in my case it needs to be passed at runtime and will be different each time the function is called.

How can I achieve this functionality?

Singles answered 13/6, 2016 at 11:55 Comment(1)
nevermind duplicate, just realized what you are trying to do.Afterclap
P
10

In your question, you've named both, the check of the user_id, as well as the wanted decorator has_permissions, so I'm going with an example where names are more clear: Let's make a decorator that calls the underlying (decorated) function when the color (a string) is 'green'.

Python decorators are function factories

The decorator itself (if_green in my example below) is a function. It takes a function to be decorated as argument (named function in my example) and returns a function (run_function_if_green in the example). Usually, the returned function calls the passed function at some point, thereby "decorating" it with other actions it might run before or after it, or both.

Of course, it might only conditionally run it, as you seem to need:

def if_green(function):
    def run_function_if_green(color, *args, **kwargs):
        if color == 'green':
            return function(*args, **kwargs)
    return run_function_if_green


@if_green
def print_if_green():
    print('what a nice color!')


print_if_green('red')  # nothing happens
print_if_green('green')  # => what a nice color!

What happens when you decorate a function with the decorator (as I did with print_if_green, here), is that the decorator (the function factory, if_green in my example) gets called with the original function (print_if_green as you see it in the code above). As is its nature, it returns a different function. Python then replaces the original function with the one returned by the decorator.

So in the subsequent calls, it's the returned function (run_function_if_green with the original print_if_green as function) that gets called as print_if_green and which conditionally calls further to that original print_if_green.

Functions factories can produce functions that take arguments

The call to the decorator (if_green) only happens once for each decorated function, not every time the decorated functions are called. But as the function returned by the decorator that one time permanently replaces the original function, it gets called instead of the original function every time that original function is invoked. And it can take arguments, if we allow it.

I've given it an argument color, which it uses itself to decide whether to call the decorated function. Further, I've given it the idiomatic vararg arguments, which it uses to call the wrapped function (if it calls it), so that I'm allowed to decorate functions taking an arbitrary number of positional and keyword arguments:

@if_green                     
def exclaim_if_green(exclamation):
    print(exclamation, 'that IS a nice color!')

exclaim_if_green('red', 'Yay')  # again, nothing
exclaim_if_green('green', 'Wow')  # => Wow that IS a nice color!

The result of decorating a function with if_green is that a new first argument gets prepended to its signature, which will be invisible to the original function (as run_function_if_green doesn't forward it). As you are free in how you implement the function returned by the decorator, it could also call the original function with less, more or different arguments, do any required transformation on them before passing them to the original function or do other crazy stuff.

Concepts, concepts, concepts

Understanding decorators requires knowledge and understanding of various other concepts of the Python language. (Most of which aren't specific to Python, but one might still not be aware of them.)

For brevity's sake (this answer is long enough as it is), I've skipped or glossed over most of them. For a more comprehensive speedrun through (I think) all relevant ones, consult e.g. Understanding Python Decorators in 12 Easy Steps!.

Porta answered 13/6, 2016 at 19:53 Comment(3)
the key here is that part of the decorator is using the arguments passed to it (color) instead of using values defined for the specific function.Afterclap
Anyone have any ideas on how to document this via the docstrings? Let's assume the decorator is defined in another file/module and a junior dev comes across the print_if_green function, they might assume it takes zero args, when in reality it expects one (referencing the first example)Kathline
A good question, @themanatuf. A question worthy of its own question post, I'd say. (Also, I'm curious what the answer(s) are, so if you post it as a question of its own, please link to it in a comment here.)Porta
A
4

The inputs to decorators (arguments, wrapped function) are rather static in python. There is no way to dynamically pass an argument like you're asking. If the user id can be extracted from somewhere at runtime inside the decorator function however, you can achieve what you want..

In Django for example, things like @login_required expect that the function they're wrapping has request as the first argument, and Request objects have a user attribute that they can utilize. Another, uglier option is to have some sort of global object you can get the current user from (see thread local storage).

Appassionato answered 13/6, 2016 at 12:4 Comment(1)
Unfortunately, I don't have such an option. I had looked at Django and Flask's decorators before asking this, but realized I don't have such a user attribute to work with.Singles
H
3

The short answer is no: you cannot pass dynamic parameters to decorators.

But... you can certainly invoke them programmatically:

First let's create a decorator that can perform a permission check before executing a function:

import functools

def check_permissions(user_id):
    def decorator(f):
        @functools.wraps(f)
        def wrapper(*args, **kw):
            if has_permissions(user_id):
                return f(*args, **kw)
            else:
                # what do you want to do if there aren't permissions?
                ...
        
         return wrapper

    return decorator

Now, when extracting an action from your dictionary, wrap it using the decorator to create a new callable that does an automatic permission check:

checked_action = check_permissions(RSSFEED['user_id'])(
    actions_dict[RSSFEED['action_taken']])

Now, when you call checked_action it will first check the permissions corresponding to the user_id before executing the underlying action.

Herophilus answered 13/6, 2016 at 12:0 Comment(4)
This requires I wrap each of my calls in has_permissions correct? Or can I decorate my functions with this as well?Singles
Looking at this again, is this recurrsive? There are two has_permissions here. Should one of them be renamed?Singles
As-is, this would indeed be recursive, which most probably isn't intended. I guess it's an artefact of the question using has_permissions as both, the name of the check(-function) to perform as well as the name of the wanted decorator that should invoke that check.Porta
@Singles it sure was! an artefact from cutting and pasting the original code in ... have now edited it so it makes a little more sense ...Herophilus
P
0

You may easily work around it, example:

from functools import wraps

def some_function():
    print("some_function executed")


def some_decorator(decorator_arg1, decorator_arg2):
    def decorate(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(decorator_arg1)
            ret = func(*args, **kwargs)
            print(decorator_arg2)
            return ret

        return wrapper

    return decorate


arg1 = "pre"
arg2 = "post"

decorated = some_decorator(arg1, arg2)(some_function)
In [4]: decorated()
pre
some_function executed
post

Parthinia answered 16/9, 2022 at 10:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.