[CentralOH] My First metaclass

Mark Erbaugh mark at microenh.com
Tue Apr 15 15:06:31 CEST 2008


I've been reading Wesley Chun's "Core Python Programming 2nd Edition"
and working on a large Python / wxPython / Postgres (psygopg2) database
application.  I wanted to simplify handling of the data retrieved from
the database, which may also be updated and written back to the
database.  psygopg2 is DBAPI 2 compliant which means and data from a
query is returned as a list (rows) of tuples (columns).  psycopg2 uses
the 'pyformat' style for parameter passing which expects a mapping to
provide parameters.

I wanted to make it easier to use the data from a query. I wanted to
track if the user had updated a row, and which columns in that row had
been updated. I needed to allow the data to track new rows that had been
inserted and rows that needed to be deleted.

Lastly, I wanted the data structure to be able to handle related "child"
data, such as lineitems on an invoice or purchase order.

I developed a system of classes that met these needs. However, it was
was a little cumbersome to use and I found myself putting off adding new
database functionality as getting everything working was tedious.

I looked at some of the Python recipes such as SuperTuple and DBTuple,
but these didn't quite meet my needs.

Then I read more about classes in Chun's book, especially the __slots__
parameter, descriptors and metaclasses.  I felt that that these might be
the key.  After a day of playing with things, I have a working class
that meets my requirements.  While the ancestor class uses some of these
advanced (to me) features, descendant classes are extremely simple, all
one needs to do is define the __slots__ attribute.  It meets my current
needs:

1) struct-like (x.colname) named column access. Note that slots are used
so a misspelled column name will trigger an exception

2) mapped access (x['colname'])

3) automatic tracking of update, insert and delete as well as tracking
which columns have been updated

4) inclusion of additional columns to handle related data.  For a parent
child relationship, this field could be a list containing instances of
another descendant class.

Here's what I came up with. Feel free to use it if appropriate, but
since this is my first foray into metaclasses, I would appreciate any
comments/criticisms.

Mark Erbaugh

==============================================

class D(object):
    """
    descriptor class

    When a data member is updated, this descriptor will automatically
    set the class's _updated member to True (indicating that the row
    has been updated) and add the column name to the _c_updated set
    (indicating that the individual column has been updated).
    """
    __slots__ = ('index','name')

    def __init__(self, index, name):
        self.index = index
        self.name = name
    
    def __get__(self, obj, typ=None):
        return obj._data[self.index]

    def __set__(self, obj, value):
        try:
            x = obj._data[self.index] <> value
        except:
            x = True
        if x:
            obj._updated = True
            obj._c_updated.add(self.name)
            obj._data[self.index] = value
        
# ---------------------------------------

class BaseMeta(type):

    def __new__(cls, name, bases, dict):
        if name <> 'Base':
            for i,n in enumerate(dict['__slots__']):
                dict[n] = D(i,n)
            del i,n
        return type.__new__(cls, name, bases, dict)
        

class Base(object):
    """
    base class for 'record-like' objects designed to hold SQL query data

    field names are the names passed in in the descendents __slots__
attribute
    fields can be accessed by name x.name, or as a mapping x['name']
        (mapping designed for dict-type parameters)
    fields _deleted and _inserted can be used to track deletes and
inserts
    fields _updated (any field updated) automatically maintained
    method c_updated(col <str>) returns True if column has been updated

    minimum for derived class:
        define field names (and order) in class __slots__ attribute

    example usage:

        class AddressType(Base):
            __slots__ = ('ctid','rsn','postal','name')

        # end of class

        cursor.execute('select ctid,rsn,postal,name from at')
        return [AddressType(i) for i in cursor.fetchall()]
        
    note: can define more fields than used in query
    """
    __metaclass__ = BaseMeta
    __slots__ = ('_updated', '_deleted', '_inserted', '_c_updated',
'_data')

    def __init__(self, data, inserted=False):
        """
        data is a sequence of field values in the same order
        as the descendent classes __slots__ attribute.

        instance's _inserted flag will be set to inserted

        The sequence may be shorter than the __slots__ attribute
        in which case trailing values will be None.
        """
        self._data = [None] * len(self.__slots__)
        self._data[:len(data)] = data
        self._updated = False
        self._c_updated = set()
        self._deleted = False
        self._inserted = inserted

    def reset_update(self):
        self._updated = False
        self._c_updated.clear()

    def __getitem__(self, x):
        return self.__getattribute__(x)

    def __setitem__(self, i, x):
        self.__setattr__(i, x)

    def c_updated(self, x):
        return x in self._c_updated

# ---------------------------------------

        
if __name__ == '__main__':
    import application.utility.db_access.se_access as DB

    class Child(Base):
        """
        descendent class example:
        minimum requirement, define field names and order
            in __slots__ attribute
        """    
        __slots__ = ('ctid','rsn','postal','name')

    DB.logon('development','dev')

    c= DB.CONN.cursor()
    c.execute("select ctid,rsn,postal,name from at")
    r = [Child(i) for i in c.fetchall()]
        
    




More information about the CentralOH mailing list