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()
'''
setup_b2 = '''
class Model(object):
    def __setattr__(self, name, value):
        self.__dict__[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()
'''

# Hyper-optimized
setup_h = '''
class Model(object):
    def __init__(self):
        self._reset_modified_attrs()
    def _reset_modified_attrs(self):
        self.__dict__['_modified_attrs'] = []
    def __setattr__(self, name, value):
        if name not in self._modified_attrs:
            self._modified_attrs.append(name)
        self.__dict__[name] = value
m = Model()
'''

# A model which has uses properties to track changes
# Bonus: only field access would be tracked this way
setup_f = '''
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()
'''

# A model which has uses a new style attribute object
# Bonus: only field access would be tracked this way
setup_g = '''
def track_property(klass, name):
    p = '_p_%s' % name
    def set_modified(o):
        try:
            if name not in o._modified_attrs: 
                o._modified_attrs.add(name)
        except AttributeError:
            o._modified_attrs = set((name,))
    class Attr(object):
        def __get__(self, instance, value):
            getattr(instance, p)
        def __set__(self, instance, value):
            set_modified(instance)
            setattr(instance, p, value)
    setattr(klass, name, Attr())
class Model(object):
    def __init__(self):
        track_property(self.__class__, 'an_attribute')
        track_property(self.__class__, 'another_attribute')
m = Model()
'''

def timed(t, control=None):
    if control is None:
        return '%.3f' % (t)
    return '%.3f (%.1fx)' % (t, t/control)

def do_test(test, extra_setup=''):
    control = Timer(test, setup_a+extra_setup).timeit()
    print "Control:  ", timed(control)
    print "noop:     ", timed(Timer(test, setup_b+extra_setup).timeit(), control)
    print "patch:    ", timed(Timer(test, setup_c+extra_setup).timeit(), control)
    print "optimized:", timed(Timer(test, setup_d+extra_setup).timeit(), control)
    print "noop-h-op:", timed(Timer(test, setup_b2+extra_setup).timeit(), control)
    print "h-optimiz:", timed(Timer(test, setup_h+extra_setup).timeit(), control)
    print "property: ", timed(Timer(test, setup_f+extra_setup).timeit(), control)
    print "new-attr: ", timed(Timer(test, setup_g+extra_setup).timeit(), control)
    
print "Single Assignment"
do_test('m.an_attribute="test"')

print
print "Double Assignment"
do_test('m.an_attribute="test";m.another_attribute="test"')

print
print "Retreiving"
do_test('m.an_attribute', extra_setup='m.an_attribute="test"')
