Search code examples
pythondecoratorpython-decorators

How to avoid using a global variable as a parameter to a decorator?


I am writing a python script where I am using a decorator(retry is the one I am using) that takes a parameter (tries). I want the parameter to be configurable from a command line argument. The only way I can figure out how to set the parameter for the decorator is by reading my arguments into a global variable. I hate this from a design perspective. It makes writing unit tests and anything else that wants to import any functions from my script reliant on the command line arguments being all the same.

Here is a dumbed down example of the problem I am having:

import argparse
from functools import wraps

def get_args():
    parser = argparse.ArgumentParser()
    parser.add_argument('-t', '--test_value', dest='test_value', required=True, default="sample value")
    args = parser.parse_args()
    return args
args = get_args()

def decorator_example(test_value):
    def deco_example(f):
        @wraps(f)
        def f_example(*args, **kwargs):
            print "The value I need is", test_value
            return f(*args, **kwargs) 
        return f_example 
    return deco_example

@decorator_example(args.test_value)
def test():
    print "running test"

if __name__ == '__main__':
    test()

If anyone can think of a better way to do this without having args be a global, please share! I have exhausted the internet searching for a better way... I want to call getargs() in main and pass the arguments around as needed....


Solution

  • I don't think a decorator is appropriate here. A class seems more suitable precisely because of the problem you're facing. Something like this:

    class Test(object):
        def __init__(self, test_value):
            self.test_value = test_value
    
        def test(self):
            print "The value I need is", self.test_value
            self._inner_test()
    
        def _inner_test():
            print "running test"
    
    
    if __name__ == '__main__':
        args = get_args()
        t = TestClass(args.test_value)
        t.test()
    

    How exactly to structure the class is not clear from the example you have given and would depend on what you're actually doing, but I think something in this vein will provide you with a more robust solution than trying to shoehorn this into decorators.

    Classes are designed to maintain state and provide modified behavior based on that state. That is what you're doing. Your application has state that modifies its behavior. Decorators are designed to wrap extra, static functionality around an existing method.

    However, if that is unsuitable, another alternative is to simply allow your arguments to be global. Something along the lines of this:

    config.py

    import argparse
    
    test_value = None
    
    def parse_args():
        parser = argparse.ArgumentParser()
        parser.add_argument('-t', '--test_value', dest='test_value', required=True, default="sample value")
        args = parser.parse_args()
        return args
    
    def configure():
        global test_value
        args = parse_args()
        test_value = args.test_value
    

    main.py

    from functools import wraps
    import config
    
    def decorator_example(f):
        @wraps(f)
        def f_example(*args, **kwargs):
            print "The value I need is", config.test_value
            return f(*args, **kwargs) 
        return f_example
    
    @decorator_example
    def test():
        print "running test"
    
    
    if __name__ == '__main__':
        config.configure()
        test()
    

    One nice side of this solution is that it gives you an obvious way to also supplement your arguments with a configuration file. Note that this should actually work since config.test_value is not actually read until test is called.