0

Say you have a configuration class (like Django's settings.py style settings) that gets set up when an application is started. You load it like so.

from myapp import config

CONF = config.CONF

You want to use some variable from this configuration as a decorator argument, like so.

@decorators.mydecorator(run_timeout=CONF.timeout)
def run_stuff_once():
    # blah, blah, blah...

@decorators.mydecorator(interval=CONF.interval)
def run_stuff_periodically():
    # blah, blah, blah...

If you run this, the configuration variables are loaded at runtime and never change. We can see this happening even without the use of decorators.

>>> class Config(object):
...     x = 5
...
>>> config = Config()
>>>
>>> def func(param=config.x):
...     print(param)
...
>>> func()
5
>>> config.x = 9
>>> func()
5

This causes two issues:

  1. If the configuration for that configuration variable is loaded after the file where this function is contained, the function uses the original value of the variable.
  2. Regardless of import order, it is not possible to change configuration dynamically once the file has been loaded.

Can this be resolved? Were I talking about "normal functions", I would adopt a solution similar to resolving PyLint's W0102 warning, i.e.

>>> def func2(param=None):
...     if param is None:
...         param = config.x
...     print(param)
...
>>> func2()
9
>>> config.x = 5
>>> func2()
5

However, it is not possible to do this for the decorator as, as seen above, the value passed to the decorator can change. I'm stumped as to a resolution for this case.

stephenfin
  • 1,447
  • 3
  • 20
  • 41
  • If it shouldn't be bound at definition time, then maybe the decorator argument isn't the way to go? – jonrsharpe Nov 03 '15 at 16:46
  • But what would be the alternative? For something like a `periodic_task` decorator (that ostensibly enough, runs a task periodically), passing a `period` parameter seems like the only way to go – stephenfin Nov 03 '15 at 16:51
  • But that's still going to happen at definition time, as that's when the function is decorated. I believe Django actually restarts the dev server when you change the config, as it can't reload everything correctly at runtime; that may be the only way to approach this. – jonrsharpe Nov 03 '15 at 16:53
  • Yes, so it does. Maybe there's no other option for it. – stephenfin Nov 03 '15 at 16:58
  • Sry, cant comment, but did u see http://stackoverflow.com/questions/3931627/how-to-build-a-python-decorator-with-optional-parameters? – Ruslan Galimov Nov 03 '15 at 17:03
  • I did. The decorator I'm actually using allows optional parameters. There is still the issue of loading these variables dynamically which, as jonrsharpe suggests, may not be possible. – stephenfin Nov 03 '15 at 17:19
  • Maybe I should just abuse `eval` :evil laugh: – stephenfin Nov 03 '15 at 17:20
  • Decorators are just functions. Why can't you apply that solution to your decorator? – knbk Nov 03 '15 at 17:41
  • @knbk Because the value can changes per instance of the decorator. For the "normal case", the value is always the same and I can therefore statically assign it to `config.x`. In the "decorator case", on the other hand, the value can be anything, such as `config.timeout` or `config.interval`. I can't figure out how to do this, besides maintaining an (unmaintainable) map of functions to default values – stephenfin Nov 03 '15 at 17:44

1 Answers1

1

This should provide a dynamic decorator that is evaluated at runtime instead of at definition time

class Config(object):
     x = 5
     y = 6

config = Config()
config2 = Config()

# takes object and atribute as arguments
def dynamic_dec(reference,atrib,kw=None):

    # gets function pointer to wraped function
    def dynamic_dec_functor(functor):

        # creates lamda function to revalueate config value
        def func_wrapper(ref_wrapper=(lambda ref=reference: getattr(ref,atrib))):

            #call original function with result of lambda
            if(kw):
                #store lambda result to keyword arg
                functor(**{kw:ref_wrapper()})
            else:
                #pass lambda result to positional arg
                functor(ref_wrapper())

        return func_wrapper

    return dynamic_dec_functor

@dynamic_dec(config,'x','param')
def func(param="default"):
    print(param)

@dynamic_dec(config2,'y')
def func2(param):
    print(param)

#time to test


print("\n call func and func2 with original config \n")

func()
func2()

print("\n update original config \n")

config.x = 9
config.y = 10

print("\n call func and func2 after config change \n")

func()
func2()

print("\n func2 did not change as it used a different config object \n")