from timeit import Timer

# A control model
setup_a = '''
class Model(object):
    pass
m = Model()
'''

# A model which has a __setattr__ which does nothing
setup_b = '''
class Model(object):
    def __setattr__(self, name, value):
        super(Model, self).__setattr__(name, value)
m = Model()
'''

# A model which has a __setattr__ which matches this patch
setup_c = '''
class Model(object):
    def __setattr__(self, name, value):
        if name != '_modified_attrs' and (not hasattr(self, name) or
                                          value != getattr(self, name)):
            if hasattr(self, '_modified_attrs'):
                if name not in self._modified_attrs: 
                    self._modified_attrs.add(name)
            else:
                self._modified_attrs = set((name,))
        super(Model, self).__setattr__(name, value)
m = Model()
'''

# Optimized for speed, and dropped the value check as Brian suggested
setup_d = '''
class Model(object):
    def __setattr__(self, name, value):
        try:
            if name not in self._modified_attrs: 
                self._modified_attrs.add(name)
        except AttributeError:
            if name != '_modified_attrs':
                self._modified_attrs = set((name,))
        super(Model, self).__setattr__(name, value)
m = Model()
'''

# A model which has uses properties to track changes
# Bonus: only field access would be tracked this way
setup_e = '''
def track_property(klass, name):
    p = '_p_%s' % name
    def fget(o):
        getattr(o, p)
    def fset(o, value):
        set_modified(o)
        setattr(o, p, value)
    def set_modified(o):
        try:
            if name not in o._modified_attrs: 
                o._modified_attrs.add(name)
        except AttributeError:
            o._modified_attrs = set((name,))
    setattr(klass, name, property(fget, fset))
class Model(object):
    def __init__(self):
        track_property(self.__class__, 'an_attribute')
        track_property(self.__class__, 'another_attribute')
m = Model()
'''

setup_a1 = setup_a + '''
m.another_attribute="test"
'''

setup_b1 = setup_b + '''
m.another_attribute="test"
'''

setup_c1 = setup_c + '''
m.another_attribute="test"
'''

setup_d1 = setup_d + '''
m.another_attribute="test"
'''

setup_e1 = setup_e + '''
m.another_attribute="test"
'''

def timed(t, control=None):
    if control is None:
        return '%.3f' % (t)
    return '%.3f (%.1fx)' % (t, t/control)

print "Single Assignment"
test = 'm.an_attribute="test"'
control = Timer(test, setup_a).timeit()
print "Control:  ", timed(control)
print "noop:     ", timed(Timer(test, setup_b).timeit(), control)
print "patch:    ", timed(Timer(test, setup_c).timeit(), control)
print "optimized:", timed(Timer(test, setup_d).timeit(), control)
print "property: ", timed(Timer(test, setup_e).timeit(), control)
print
print "Double Assignment"
test = 'm.an_attribute="test";m.another_attribute="test"'
control = Timer(test, setup_a).timeit()
print "Control:  ", timed(control)
print "noop:     ", timed(Timer(test, setup_b).timeit(), control)
print "patch:    ", timed(Timer(test, setup_c).timeit(), control)
print "optimized:", timed(Timer(test, setup_d).timeit(), control)
print "property: ", timed(Timer(test, setup_e).timeit(), control)
