Wednesday, July 8, 2009

A Self-Migrating Descriptor

Imagine that you have a very plain python class definition.

class MyClass(object):
pass

You create an instance of that class and assign attribute named 'x' the value 'quux'.

o = MyClass()
o.x = 'quux'

Now you monkey patch the class definition assigning a descriptor to the x attribute.

class BasicDescriptor(object):

def __init__(self, id):
self.id = id

def __get__(self, instance, owner):
if instance is None:
retval = self
else:
retval = 'foo_' + instance._values[self.id]
return retval

def __set__(self, instance, value):
if not hasattr(instance, '_values'):
instance._values = {}
instance._values[self.id] = 'bar_' + value

def __delete__(self, instance):
del instance._values[self.id]

MyClass.x = BasicDescriptor('x')

Accessing x on the instance now results in an AttributeError. A solution to this problem, is to create a self-migrating descriptor that copies values from __dict__ to the _values dictionary that the descriptor uses.

class MigratingDescriptor(object):

def __init__(self, id):
self.id = id

def __get__(self, instance, owner):
if instance is None:
retval = self
else:
self._migrate(instance)
retval = 'foo_' + instance._values[self.id]
return retval

def __set__(self, instance, value):
self._migrate(instance)
self._set(instance, value)

def __delete__(self, instance):
self._migrate(instance)
del instance._values[self.id]

def _migrate(self, instance):
if not hasattr(instance, '_values'):
instance._values = {}
if self.id in vars(instance):
if self.id in instance._values:
raise Exception('%s is in vars and _values' % self.id)
self._set(instance, vars(instance)[self.id])
del instance.__dict__[self.id]

def _set(self, instance, value):
instance._values[self.id] = 'bar_' + value

This is a real world problem. I ran into it today when trying to migrate persistent classes that are stored in a Zope object database. I'm changing the class definition for the next release of Zenoss to include descriptors for the attributes.

The full source code for this example including unit tests is on the Zenoss Trac.