This decorator runs a function/method the first time called, and on any subsequent calls when some user-defined metric changes. The metric function provided to the decorator is called every time, but the function being decorated is only called on the first call and when the metric changes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | def func_onchange(metric):
def _inner_onchange(func):
"A decorator that runs a function only when a generic metric changes."
def decorated(*args, **kwargs):
try:
mresult = metric(*args, **kwargs)
if decorated._last_metric != mresult:
decorated._last_metric = mresult
decorated._last_result = func(*args, **kwargs)
except AttributeError:
decorated._last_metric = mresult
decorated._last_result = func(*args, **kwargs)
return decorated._last_result
return decorated
return _inner_onchange
def method_onchange(metric):
def _inner_onchange(method):
"A decorator that runs a method only when a generic metric changes."
met_name = "_%s_last_metric" % id(method)
res_name = "_%s_last_result" % id(method)
def decorated(self, *args, **kwargs):
try:
mresult = metric(*args, **kwargs)
if getattr(decorated, met_name) != mresult:
setattr(decorated, met_name, mresult)
setattr(decorated, res_name, method(self, *args, **kwargs))
except AttributeError:
setattr(decorated, met_name, mresult)
setattr(decorated, res_name, method(self, *args, **kwargs))
return getattr(decorated, res_name)
return decorated
return _inner_onchange
# simple check to see if file changed
def cheezy_file_metric(filename):
import os
return os.stat(filename)
# Function example, will only read file contents when file stats change
@func_onchange(cheezy_file_metric)
def get_filecontents(filename):
print 'reading "%s" contents' % filename
return file(filename).read()
file('test.txt', 'w').write('original content')
print '-'*60
print get_filecontents('test.txt')
print '-'*60
print get_filecontents('test.txt')
file('test.txt', 'w').write('new and improved content!')
print '-'*60
print get_filecontents('test.txt')
print '-'*60
print get_filecontents('test.txt')
print '~'*60
# Method example, will only read file contents when file stats change
class FileReadThing:
@method_onchange(cheezy_file_metric)
def get_filecontents(self, filename):
print 'reading "%s" contents' % filename
return file(filename).read()
f = FileReadThing()
file('test.txt', 'w').write('original content')
print '-'*60
print f.get_filecontents('test.txt')
print '-'*60
print f.get_filecontents('test.txt')
file('test.txt', 'w').write('new and improved content!')
print '-'*60
print f.get_filecontents('test.txt')
print '-'*60
print f.get_filecontents('test.txt')
|
I needed to implement a "reload when file content/time changes" feature in a couple of places in an application at work, and I was about to do it the copy/paste/edit way (silly, I know) until I happened to see Ori Peleg's recipe (http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/425445) that appeared on the Daily Python-URL yesterday. Since it didn't seem like it would be hard to build some general-purpose "on change" decorators from Ori's recipe, I tweaked on it for a bit, and this is the result.
It seems to me that there should be some way to remove the duplication of code between the function and method versions of the decorator, and the duplication of those two lines within the try/except blocks, but I don't have time to figure that out at the moment. Please comment if you can see an elegant way to do it.