Commits

Rufus Pollock committed 7ad4998

[sqlalchemy][s]: strip out all remaining old code and move test_tools.py into vdm/test/sqlalchemy.

Comments (0)

Files changed (10)

vdm/sqlalchemy/__init__.py

 
 2. Support for composite primary keys.
 '''
-from .base import SQLAlchemySession
 from .tools import Repository
 from .changeset import Changeset, ChangeObject, setup_changeset
 from .model import VersionedListener
-from .sqla import SQLAlchemyMixin
+from .sqla import SQLAlchemyMixin, SQLAlchemySession
 

vdm/sqlalchemy/base.py

-from datetime import datetime
-import difflib
-import uuid
-import logging
-import weakref
-
-from sqlalchemy import *
-from sqlalchemy.orm.attributes import get_history
-from sqlalchemy import __version__ as sqav
-
-from sqla import SQLAlchemyMixin
-from sqla import copy_column, copy_table_columns, copy_table
-
-make_uuid = lambda: unicode(uuid.uuid4())
-logger = logging.getLogger('vdm')
-
-## -------------------------------------
-class SQLAlchemySession(object):
-    '''Handle setting/getting attributes on the SQLAlchemy session.
-    
-    TODO: update all methods so they can take an object as well as session
-    object.
-    '''
-
-    @classmethod
-    def setattr(self, session, attr, value):
-        setattr(session, attr, value)
-        # check if we are being given the Session class (threadlocal case)
-        # if so set on both class and instance
-        # this is important because sqlalchemy's object_session (used below) seems
-        # to return a Session() not Session
-        if isinstance(session, sqlalchemy.orm.scoping.ScopedSession):
-            sess = session()
-            setattr(sess, attr, value)
-
-    @classmethod
-    def getattr(self, session, attr):
-        return getattr(session, attr)
-
-    # make explicit to avoid errors from typos (no attribute defns in python!)
-    @classmethod
-    def set_revision(self, session, revision):
-        self.setattr(session, 'HEAD', True)
-        self.setattr(session, 'revision', revision)
-        if revision.id is None:
-            # make uuid here so that if other objects in this session are flushed
-            # at the same time they know thier revision id
-            revision.id = make_uuid()
-            # there was a begin_nested here but that just caused flush anyway.
-            session.add(revision)
-            session.flush()
-
-    @classmethod
-    def get_revision(self, session):
-        '''Get revision on current Session/session.
-        
-        NB: will return None if not set
-        '''
-        return getattr(session, 'revision', None)
-
-    @classmethod
-    def set_not_at_HEAD(self, session):
-        self.setattr(session, 'HEAD', False)
-
-    @classmethod
-    def at_HEAD(self, session):
-        return getattr(session, 'HEAD', True)
-
-
-## --------------------------------------------------------
-## VDM-Specific Domain Objects and Tables
-
-# Enumeration
-class State(object):
-    ACTIVE = u'active'
-    DELETED = u'deleted'
-    PENDING = u'pending'
-    all = (ACTIVE, DELETED, PENDING)
-
-def make_revision_table(metadata):
-    revision_table = Table('revision', metadata,
-            Column('id', UnicodeText, primary_key=True, default=make_uuid),
-            Column('timestamp', DateTime, default=datetime.now),
-            Column('author', String(200)),
-            Column('message', UnicodeText),
-            Column('state', UnicodeText, default=State.ACTIVE)
-            )
-    return revision_table
-
-
-class Revision(SQLAlchemyMixin):
-    '''A Revision to the Database/Domain Model.
-
-    All versioned objects have an associated Revision which can be accessed via
-    the revision attribute.
-    '''
-    # TODO:? set timestamp in ctor ... (maybe not as good to have undefined
-    # until actual save ...)
-    @property
-    def __id__(self):
-        if self.id is None:
-            self.id = make_uuid()
-        return self.id
-    @classmethod
-    def youngest(self, session):
-        '''Get the youngest (most recent) revision.
-
-        If session is not provided assume there is a contextual session.
-        '''
-        q = session.query(self)
-        return q.first()
-
-
-def make_Revision(mapper, revision_table):
-    mapper(Revision, revision_table, properties={
-        },
-        order_by=revision_table.c.timestamp.desc())
-    return Revision
-
-## --------------------------------------------------------
-## Table Helpers
-
-def make_table_stateful(base_table):
-    '''Make a table 'stateful' by adding appropriate state column.'''
-    base_table.append_column(
-        Column('state', UnicodeText, default=State.ACTIVE)
-        )
-
-def make_table_revisioned(base_table):
-    logger.warn('make_table_revisioned is deprecated: use make_revisioned_table')
-    return make_revisioned_table(base_table)
-
-def make_revisioned_table(base_table):
-    '''Modify base_table and create correponding revision table.
-
-    # TODO: (complex) support for complex primary keys on continuity. 
-    # Search for "composite foreign key sqlalchemy" for helpful info
-
-    @return revision table.
-    '''
-    base_table.append_column(
-            Column('revision_id', UnicodeText, ForeignKey('revision.id'))
-            )
-    newtable = Table(base_table.name + '_revision', base_table.metadata,
-            )
-    copy_table(base_table, newtable)
-
-    # create foreign key 'continuity' constraint
-    # remember base table primary cols have been exactly duplicated onto our table
-    pkcols = []
-    for col in base_table.c:
-        if col.primary_key:
-            pkcols.append(col)
-    if len(pkcols) > 1:
-        msg = 'Do not support versioning objects with multiple primary keys'
-        raise ValueError(msg)
-    fk_name = base_table.name + '.' + pkcols[0].name
-    newtable.append_column(
-        Column('continuity_id', pkcols[0].type, ForeignKey(fk_name))
-        )
-    # TODO: a start on composite primary key stuff
-    # newtable.append_constraint(
-    #        ForeignKeyConstraint(
-    #            [c.name for c in pkcols],
-    #            [base_table.name + '.' + c.name for c in pkcols ]
-    #    ))
-
-    # TODO: why do we iterate all the way through rather than just using dict
-    # functionality ...? Surely we always have a revision here ...
-    for col in newtable.c:
-        if col.name == 'revision_id':
-            col.primary_key = True
-            newtable.primary_key.columns.add(col)
-    return newtable
-
-
-## --------------------------------------------------------
-## Object Helpers
-
-class StatefulObjectMixin(object):
-    __stateful__ = True
-
-    def delete(self):
-        logger.debug('Running delete on %s' % self)
-        self.state = State.DELETED
-    
-    def undelete(self):
-        self.state = State.ACTIVE
-
-    def is_active(self):
-        # also support None in case this object is not yet refreshed ...
-        return self.state is None or self.state == State.ACTIVE
-
-
-class RevisionedObjectMixin(object):
-    __ignored_fields__ = ['revision_id']
-    __revisioned__ = True
-
-    @classmethod
-    def revisioned_fields(cls):
-        table = sqlalchemy.orm.class_mapper(cls).mapped_table
-        fields = [ col.name for col in table.c if col.name not in
-                cls.__ignored_fields__ ]
-        return fields
-
-    def get_as_of(self, revision=None):
-        '''Get this domain object at the specified revision.
-        
-        If no revision is specified revision will be looked up on the global
-        session object. If that not found return head.
-
-        get_as_of does most of the crucial work in supporting the
-        versioning.
-        '''
-        sess = object_session(self)
-        if revision: # set revision on the session so dom traversal works
-            # TODO: should we test for overwriting current session?
-            # if rev != revision:
-            #     msg = 'The revision on the session does not match the one you' + \
-            #     'requesting.'
-            #     raise Exception(msg)
-            logger.debug('get_as_of: setting revision and not_as_HEAD: %s' %
-                    revision)
-            SQLAlchemySession.set_revision(sess, revision)
-            SQLAlchemySession.set_not_at_HEAD(sess)
-        else:
-            revision = SQLAlchemySession.get_revision(sess)
-
-        if SQLAlchemySession.at_HEAD(sess):
-            return self
-        else:
-            revision_class = self.__revision_class__
-            # TODO: when dealing with multi-col pks will need to update this
-            # (or just use continuity)
-            out = sess.query(revision_class).join('revision').\
-                filter(
-                    Revision.timestamp <= revision.timestamp
-                ).\
-                filter(
-                    revision_class.id == self.id
-                ).\
-                order_by(
-                    Revision.timestamp.desc()
-                )
-            return out.first()
-    
-    @property
-    def all_revisions(self):
-        allrevs = self.all_revisions_unordered
-        ourcmp = lambda revobj1, revobj2: cmp(revobj1.revision.timestamp,
-                revobj2.revision.timestamp)
-        sorted_revobjs = sorted(allrevs, cmp=ourcmp, reverse=True)
-        return sorted_revobjs
-
-    def diff(self, to_revision=None, from_revision=None):
-        '''Diff this object returning changes between `from_revision` and
-        `to_revision`.
-
-        @param to_revision: revision to diff to (defaults to the youngest rev)
-        @param from_revision: revision to diff from (defaults to one revision
-        older than to_revision)
-        @return: dict of diffs keyed by field name
-
-        e.g. diff(HEAD, HEAD-2) will show diff of changes made in last 2
-        commits (NB: no changes may have occurred to *this* object in those
-        commits).
-        '''
-        obj_rev_class = self.__revision_class__
-        sess = object_session(self)
-        obj_rev_query = sess.query(obj_rev_class).join('revision').\
-                        filter(obj_rev_class.id==self.id).\
-                        order_by(Revision.timestamp.desc())
-        obj_class = self
-        to_obj_rev, from_obj_rev = self.get_obj_revisions_to_diff(\
-            obj_rev_query,
-            to_revision=to_revision,
-            from_revision=from_revision)
-        return self.diff_revisioned_fields(to_obj_rev, from_obj_rev,
-                                           obj_class)
-
-    
-    def get_obj_revisions_to_diff(self, obj_revision_query, to_revision=None,
-                           from_revision=None):
-        '''Diff this object returning changes between `from_revision` and
-        `to_revision`.
-
-        @param obj_revision_query: query of all object revisions related to
-        the object being diffed. e.g. all PackageRevision objects with 
-        @param to_revision: revision to diff to (defaults to the youngest rev)
-        @param from_revision: revision to diff from (defaults to one revision
-        older than to_revision)
-        @return: dict of diffs keyed by field name
-
-        e.g. diff(HEAD, HEAD-2) will show diff of changes made in last 2
-        commits (NB: no changes may have occurred to *this* object in those
-        commits).
-        '''
-        sess = object_session(self)
-        if to_revision is None:
-            to_revision = Revision.youngest(sess)
-        out = obj_revision_query.\
-              filter(Revision.timestamp<=to_revision.timestamp)
-        to_obj_rev = out.first()
-        if not from_revision:
-            from_revision = sess.query(Revision).\
-                filter(Revision.timestamp<to_revision.timestamp).first()
-        # from_revision may be None, e.g. if to_revision is rev when object was
-        # created
-        if from_revision:
-            out = obj_revision_query.\
-                filter(Revision.timestamp<=from_revision.timestamp)
-            from_obj_rev = out.first()
-        else:
-            from_obj_rev = None
-        return to_obj_rev, from_obj_rev
-    
-    @classmethod
-    def diff_revisioned_fields(self, to_obj_rev, from_obj_rev, obj_class):
-        '''
-        Given two object revisions (e.g. PackageRevisions), diffs the
-        revisioned fields.
-        @to_obj_rev final object revision to diff
-        @from_obj_rev original object revision to diff
-        @param obj_class: class of object
-        @return dict of the diffs, keyed by the field names
-        '''
-        diffs = {}
-        fields = obj_class.revisioned_fields()
-
-        for field in fields:
-            # allow None on getattr since rev may be None (see above)
-            values = [getattr(obj_rev, field, None) for obj_rev in [from_obj_rev, to_obj_rev]]
-            diff = self._differ(values[0], values[1])
-            if diff:
-                diffs[field] = diff
-        return diffs
-
-    @classmethod
-    def _differ(self, str_a, str_b):
-        str_a = unicode(str_a)
-        str_b = unicode(str_b)
-        if str_a != str_b:
-            return '\n'.join(difflib.Differ().compare(str_a.split('\n'), str_b.split('\n')))
-        else:
-            return None
-
-
-
-## --------------------------------------------------------
-## Mapper Helpers
-
-import sqlalchemy.orm.properties
-from sqlalchemy.orm import class_mapper
-from sqlalchemy.orm import relation, backref
-
-def modify_base_object_mapper(base_object, revision_obj, state_obj):
-    base_mapper = class_mapper(base_object)
-    base_mapper.add_property('revision', relation(revision_obj))
-
-def create_object_version(mapper_fn, base_object, rev_table):
-    '''Create the Version Domain Object corresponding to base_object.
-
-    E.g. if Package is our original object we should do::
-    
-        # name of Version Domain Object class 
-        PackageVersion = create_object_version(..., Package, ...)
-    
-    NB: This must obviously be called after mapping has happened to
-    base_object.
-    '''
-    # TODO: can we always assume all versioned objects are stateful?
-    # If not need to do an explicit check
-    class MyClass(StatefulObjectMixin, SQLAlchemyMixin):
-        pass
-
-    name = base_object.__name__ + 'Revision'
-    MyClass.__name__ = name
-    MyClass.__continuity_class__ = base_object
-
-    # Must add this so base object can retrieve revisions ...
-    base_object.__revision_class__ = MyClass
-
-    ourmapper = mapper_fn(MyClass, rev_table, properties={
-        # NB: call it all_revisions_... rather than just revisions_... as it
-        # will yield all revisions not just those less than the current
-        # revision
-        'continuity':relation(base_object,
-            backref=backref('all_revisions_unordered',
-                cascade='all, delete, delete-orphan'),
-                order_by=rev_table.c.revision_id.desc()
-            ),
-        # 'continuity':relation(base_object),
-        },
-        order_by=[rev_table.c.continuity_id, rev_table.c.revision_id.desc()]
-        )
-    base_mapper = class_mapper(base_object)
-    # add in 'relationship' stuff from continuity onto revisioned obj
-    # 3 types of relationship
-    # 1. scalar (i.e. simple fk)
-    # 2. list (has many) (simple fk the other way)
-    # 3. list (m2m) (join table)
-    # 
-    # Also need to check whether related object is revisioned
-    # 
-    # If related object is revisioned then can do all of these
-    # If not revisioned can only support simple relation (first case -- why?)
-    for prop in base_mapper.iterate_properties:
-        is_relation = prop.__class__ == sqlalchemy.orm.properties.PropertyLoader
-        if is_relation:
-            # in sqlachemy 0.4.2
-            # prop_remote_obj = prop.select_mapper.class_
-            # in 0.4.5
-            prop_remote_obj = prop.argument
-            remote_obj_is_revisioned = getattr(prop_remote_obj, '__revisioned__', False)
-            # this is crude, probably need something better
-            is_many = (prop.secondary != None or prop.uselist)
-            if remote_obj_is_revisioned:
-                propname = prop.key
-                add_fake_relation(MyClass, propname, is_many=is_many)
-            elif not is_many:
-                ourmapper.add_property(prop.key, relation(prop_remote_obj))
-            else:
-                # TODO: actually deal with this
-                # raise a warning of some kind
-                msg = 'Skipping adding property %s to revisioned object' % prop 
-                logger.warn(msg)
-
-    return MyClass
-
-def add_fake_relation(revision_class, name, is_many=False): 
-    '''Add a 'fake' relation on ObjectRevision objects.
-    
-    These relation are fake in that they just proxy to the continuity object
-    relation.
-    '''
-    def _pget(self):
-        related_object = getattr(self.continuity, name)
-        if is_many:
-            # do not need to do anything to get to right revision since either
-            # 1. this is implemented inside the is_many relation we proxy to
-            # (as is the case with StatefulLists and assoc proxy setup as used
-            # in add_stateful_versioned_m2m)
-            # 2. it is not because it is not appropriate to apply it
-            # (e.g. package.package_tags which points to PackageTag objects and
-            # which is not versioned here ...)
-            return related_object
-        else:
-            return related_object.get_as_of()
-    x = property(_pget)
-    setattr(revision_class, name, x)
-
-from stateful import add_stateful_m2m
-def add_stateful_versioned_m2m(*args, **kwargs):
-    '''Add a Stateful versioned m2m attributes to a domain object.
-    
-    For args and kwargs see add_stateful_m2m.
-    '''
-    def get_as_of(obj):
-        return obj.get_as_of()
-
-    newkwargs = dict(kwargs)
-    newkwargs['base_modifier'] = get_as_of
-    add_stateful_m2m(*args, **newkwargs)
-
-def add_stateful_versioned_m2m_on_version(revision_class, m2m_property_name):
-    # just add these m2m properties to version
-    active_name = m2m_property_name + '_active'
-    deleted_name = m2m_property_name + '_deleted'
-    for propname in [active_name, deleted_name, m2m_property_name]:
-        add_fake_relation(revision_class, propname,
-                is_many=True)
-
-
-from sqlalchemy.orm import MapperExtension
-from sqlalchemy.orm import object_session
-from sqlalchemy.orm import EXT_CONTINUE
-
-class Revisioner(MapperExtension):
-    '''SQLAlchemy MapperExtension which implements revisioning of sqlalchemy
-    mapped objects.
-    
-    In essence it implements copy on write.
-
-    However various additional features such as:
-    
-        * Checking for 'real' changes -- often sqlalchemy objects are marked as
-          changed when not (just a related attribute has changed).
-        * support for ignored attributes (these attributes will be ignored when
-          checking for changes and creating new revisions of the object)
-    '''
-
-    def __init__(self, revision_table):
-        self.revision_table = revision_table
-        # Sometimes (not predictably) the after_update method is called
-        # *after* the next instance's before_update! So to avoid this,
-        # we store the instance with the is_changed flag.
-        # It is a weak key dictionary to make sure the instance is garbage
-        # collected.
-        self._is_changed = weakref.WeakKeyDictionary() # instance:is_changed
-
-    def revisioning_disabled(self, instance):
-        # logger.debug('revisioning_disabled: %s' % instance)
-        sess = object_session(instance)
-        disabled = getattr(sess, 'revisioning_disabled', False)
-        return disabled
-
-    def set_revision(self, instance):
-        sess = object_session(instance)
-        current_rev = SQLAlchemySession.get_revision(sess) 
-        # was using revision_id but this led to weird intermittent erros
-        # (1/3: fail on first item, 1/3 on second, 1/3 ok).
-        # assert current_rev.id
-        # instance.revision_id = current_rev.id
-        # LATER: this resulted (I think) from setting revision_id but not
-        # setting revision on the object
-
-        # In fact must do *both* Why?
-        # SQLAlchemy mapper extension methods can only make changes to columns.
-        # Any changes make to relations will not be picked up (from docs):
-        # "Column-based attributes can be modified within this method which will
-        # result in their being updated. However no changes to the overall
-        # flush plan can be made; this means any collection modification or
-        # save() operations which occur within this method will not take effect
-        # until the next flush call."
-        #
-        # Thus: set revision_id to ensure that value is saved
-        # set revision to ensure object behaves how it should (e.g. we use
-        # instance.revision in after_update)
-        assert current_rev, 'No revision is currently set for this Session'
-        # We need the revision id unfortunately for this all to work
-        # Why? We cannot created new sqlachemy objects in here (as they won't
-        # get saved). This means we have to created revision_object directly in
-        # database which requires we use *column* values. In particular, we
-        # need revision_id not revision object to create revision_object
-        # properly!
-        logger.debug('Revisioner.set_revision: revision is %s' % current_rev)
-        assert current_rev.id, 'Must have a revision.id to create object revision'
-        instance.revision = current_rev
-        # must set both since we are already in flush so setting object will
-        # not be enough
-        instance.revision_id = current_rev.id
-
-    def check_real_change(self, instance, mapper, connection):
-        # check each attribute to see if they have been changed
-        logger.debug('check_real_change: %s' % instance)
-        if sqav.startswith("0.4"):
-            state = instance._state
-        else:
-            state = instance
-        for key in instance.revisioned_fields():
-            (added, unchanged, deleted) = get_history(state,
-                                                      key,
-                                                      passive = False)
-            if added or deleted:
-                logger.debug('check_real_change: True')
-                return True
-        logger.debug('check_real_change: False')
-        return False
-
-    def make_revision(self, instance, mapper, connection):
-        # NO GOOD working with the object as that only gets committed at next
-        # flush. Need to work with the table directly
-        colvalues = {}
-        table = mapper.tables[0]
-        for key in table.c.keys():
-            val = getattr(instance, key)
-            colvalues[key] = val
-        # because it is unlikely instance has been refreshed at this point the
-        # fk revision_id is not yet set on this object so get it directly
-        assert instance.revision.id
-        colvalues['revision_id'] = instance.revision.id
-        colvalues['continuity_id'] = instance.id
-
-        # Allow for multiple SQLAlchemy flushes/commits per VDM revision
-        revision_already_query = self.revision_table.count()
-        existing_revision_clause = and_(
-                self.revision_table.c.continuity_id == instance.id,
-                self.revision_table.c.revision_id == instance.revision.id)
-        revision_already_query = revision_already_query.where(
-                existing_revision_clause
-                )
-        num_revisions = connection.execute(revision_already_query).scalar()
-        revision_already = num_revisions > 0
-
-        if revision_already:
-            logger.debug('Updating version of %s: %s' % (instance, colvalues))
-            connection.execute(self.revision_table.update(existing_revision_clause).values(colvalues))
-        else:
-            logger.debug('Creating version of %s: %s' % (instance, colvalues))
-            ins = self.revision_table.insert().values(colvalues)
-            connection.execute(ins)
-
-        # set to None to avoid accidental reuse
-        # ERROR: cannot do this as after_* is called per object and may be run
-        # before_update on other objects ...
-        # probably need a SessionExtension to deal with this properly
-        # object_session(instance).revision = None
-
-    def before_update(self, mapper, connection, instance):
-        self._is_changed[instance] = self.check_real_change(instance, mapper, connection)
-        if not self.revisioning_disabled(instance) and self._is_changed[instance]:
-            logger.debug('before_update: %s' % instance)
-            self.set_revision(instance)
-            self._is_changed[instance] = self.check_real_change(
-                instance, mapper, connection)
-        return EXT_CONTINUE
-
-    # We do most of the work in after_insert/after_update as at that point
-    # instance has been properly created (which means e.g. instance.id is
-    # available ...)
-    def before_insert(self, mapper, connection, instance):
-        self._is_changed[instance] = self.check_real_change(instance, mapper, connection)
-        if not self.revisioning_disabled(instance) and self._is_changed[instance]:
-            logger.debug('before_insert: %s' % instance)
-            self.set_revision(instance)
-        return EXT_CONTINUE
-
-    def after_update(self, mapper, connection, instance):
-        if not self.revisioning_disabled(instance) and self._is_changed[instance]:
-            logger.debug('after_update: %s' % instance)
-            self.make_revision(instance, mapper, connection)
-        return EXT_CONTINUE
-
-    def after_insert(self, mapper, connection, instance):
-        if not self.revisioning_disabled(instance) and self._is_changed[instance]:
-            logger.debug('after_insert: %s' % instance)
-            self.make_revision(instance, mapper, connection)
-        return EXT_CONTINUE
-
-    def append_result(self, mapper, selectcontext, row, instance, result,
-             **flags):
-        # TODO: 2009-02-13 why is this needed? Can we remove this?
-        return EXT_CONTINUE
-

vdm/sqlalchemy/demo.py

-'''Demo of vdm for SQLAlchemy.
-
-This module sets up a small domain model with some versioned objects. Code
-that then uses these objects can be found in demo_test.py.
-'''
-from datetime import datetime
-import logging
-logger = logging.getLogger('vdm')
-
-from sqlalchemy import *
-from sqlalchemy import __version__ as sqla_version
-# from sqlalchemy import create_engine
-
-import vdm.sqlalchemy
-
-TEST_ENGINE = "postgres"  # or "sqlite"
-
-if TEST_ENGINE == "postgres":
-    engine = create_engine('postgres://tester:pass@localhost/vdmtest',
-                           pool_threadlocal=True)
-else:
-    # setting the isolation_level is a hack required for sqlite support
-    # until http://code.google.com/p/pysqlite/issues/detail?id=24 is
-    # fixed.
-    engine = create_engine('sqlite:///:memory:',
-                           connect_args={'isolation_level': None})
-
-metadata = MetaData(bind=engine)
-
-## VDM-specific tables
-
-revision_table = vdm.sqlalchemy.make_revision_table(metadata)
-
-## Demo tables
-
-license_table = Table('license', metadata,
-        Column('id', Integer, primary_key=True),
-        Column('name', String(100)),
-        Column('open', Boolean),
-        )
-
-import uuid
-def uuidstr(): return str(uuid.uuid4())
-package_table = Table('package', metadata,
-        # Column('id', Integer, primary_key=True),
-        Column('id', String(36), default=uuidstr, primary_key=True),
-        Column('name', String(100), unique=True),
-        Column('title', String(100)),
-        Column('license_id', Integer, ForeignKey('license.id')),
-        Column('notes', UnicodeText),
-)
-
-tag_table = Table('tag', metadata,
-        Column('id', Integer, primary_key=True),
-        Column('name', String(100)),
-)
-
-package_tag_table = Table('package_tag', metadata,
-        Column('id', Integer, primary_key=True),
-        # Column('package_id', Integer, ForeignKey('package.id')),
-        Column('package_id', String(36), ForeignKey('package.id')),
-        Column('tag_id', Integer, ForeignKey('tag.id')),
-        )
-
-
-vdm.sqlalchemy.make_table_stateful(license_table)
-vdm.sqlalchemy.make_table_stateful(package_table)
-vdm.sqlalchemy.make_table_stateful(tag_table)
-vdm.sqlalchemy.make_table_stateful(package_tag_table)
-license_revision_table = vdm.sqlalchemy.make_revisioned_table(license_table)
-package_revision_table = vdm.sqlalchemy.make_revisioned_table(package_table)
-# TODO: this has a composite primary key ...
-package_tag_revision_table = vdm.sqlalchemy.make_revisioned_table(package_tag_table)
-
-
-
-## -------------------
-## Mapped classes
-
-        
-class License(vdm.sqlalchemy.RevisionedObjectMixin,
-    vdm.sqlalchemy.StatefulObjectMixin,
-    vdm.sqlalchemy.SQLAlchemyMixin
-    ):
-    def __init__(self, **kwargs):
-        for k,v in kwargs.items():
-            setattr(self, k, v)
-
-class Package(vdm.sqlalchemy.RevisionedObjectMixin,
-        vdm.sqlalchemy.StatefulObjectMixin,
-        vdm.sqlalchemy.SQLAlchemyMixin
-        ):
-
-    def __init__(self, **kwargs):
-        for k,v in kwargs.items():
-            setattr(self, k, v)
-
-
-class Tag(vdm.sqlalchemy.SQLAlchemyMixin):
-    def __init__(self, name):
-        self.name = name
-
-
-class PackageTag(vdm.sqlalchemy.RevisionedObjectMixin,
-        vdm.sqlalchemy.StatefulObjectMixin,
-        vdm.sqlalchemy.SQLAlchemyMixin
-        ):
-    def __init__(self, package=None, tag=None, state=None, **kwargs):
-        logger.debug('PackageTag.__init__: %s, %s' % (package, tag))
-        self.package = package
-        self.tag = tag
-        self.state = state
-        for k,v in kwargs.items():
-            setattr(self, k, v)
-
-
-## --------------------------------------------------------
-## Mapper Stuff
-
-from sqlalchemy.orm import scoped_session, sessionmaker, create_session
-from sqlalchemy.orm import relation, backref
-# both options now work
-# Session = scoped_session(sessionmaker(autoflush=False, transactional=True))
-# this is the more testing one ...
-if sqla_version <= '0.4.99':
-    Session = scoped_session(sessionmaker(autoflush=True, transactional=True))
-else:
-    Session = scoped_session(sessionmaker(autoflush=True,
-                                          expire_on_commit=False,
-                                          autocommit=False))
-
-# mapper = Session.mapper
-from sqlalchemy.orm import mapper
-
-# VDM-specific domain objects
-State = vdm.sqlalchemy.State
-Revision = vdm.sqlalchemy.make_Revision(mapper, revision_table)
-
-mapper(License, license_table, properties={
-    },
-    extension=vdm.sqlalchemy.Revisioner(license_revision_table)
-    )
-
-mapper(Package, package_table, properties={
-    'license':relation(License),
-    # delete-orphan on cascade does NOT work!
-    # Why? Answer: because of way SQLAlchemy/our code works there are points
-    # where PackageTag object is created *and* flushed but does not yet have
-    # the package_id set (this cause us other problems ...). Some time later a
-    # second commit happens in which the package_id is correctly set.
-    # However after first commit PackageTag does not have Package and
-    # delete-orphan kicks in to remove it!
-    # 
-    # do we want lazy=False here? used in:
-    # <http://www.sqlalchemy.org/trac/browser/sqlalchemy/trunk/examples/association/proxied_association.py>
-    'package_tags':relation(PackageTag, backref='package', cascade='all'), #, delete-orphan'),
-    },
-    extension = vdm.sqlalchemy.Revisioner(package_revision_table)
-    )
-
-mapper(Tag, tag_table)
-
-mapper(PackageTag, package_tag_table, properties={
-    'tag':relation(Tag),
-    },
-    extension = vdm.sqlalchemy.Revisioner(package_tag_revision_table)
-    )
-
-vdm.sqlalchemy.modify_base_object_mapper(Package, Revision, State)
-vdm.sqlalchemy.modify_base_object_mapper(License, Revision, State)
-vdm.sqlalchemy.modify_base_object_mapper(PackageTag, Revision, State)
-PackageRevision = vdm.sqlalchemy.create_object_version(mapper, Package,
-        package_revision_table)
-LicenseRevision = vdm.sqlalchemy.create_object_version(mapper, License,
-        license_revision_table)
-PackageTagRevision = vdm.sqlalchemy.create_object_version(mapper, PackageTag,
-        package_tag_revision_table)
-
-from base import add_stateful_versioned_m2m 
-vdm.sqlalchemy.add_stateful_versioned_m2m(Package, PackageTag, 'tags', 'tag',
-        'package_tags')
-vdm.sqlalchemy.add_stateful_versioned_m2m_on_version(PackageRevision, 'tags')
-
-## ------------------------
-## Repository helper object
-
-from tools import Repository
-repo = Repository(metadata, Session,
-        versioned_objects = [ Package, License,  PackageTag ]
-        )
-

vdm/sqlalchemy/demo_meta.py

-'''SQLAlchemy Metadata and Session object'''
-from sqlalchemy import MetaData
-from sqlalchemy.orm import scoped_session, sessionmaker
-
-__all__ = ['Session', 'engine', 'metadata', 'init_with_engine' ]
-
-engine = None
-
-# IMPORTANT NOTE for vdm
-# You cannot use autoflush=True, autocommit=False
-# This is because flushes to the DB may then happen at 'random' times when no
-# Revision has yet been set which may result in errors
-Session = scoped_session(sessionmaker(
-    autoflush=True,
-    # autocommit=False,
-    transactional=True,
-    ))
-
-# Global metadata. If you have multiple databases with overlapping table
-# names, you'll need a metadata for each database
-metadata = MetaData()
-
-def init_with_engine(engine_):
-    metadata.bind = engine_
-    Session.configure(bind=engine_)
-    engine = engine_
-

vdm/sqlalchemy/demo_simple.py

-'''A simple demo of vdm.
-
-This demo shows how to use vdm for simple versioning of individual domain
-objects (without any versioning of the relations between objects). For more
-complex example see demo.py
-'''
-
-from sqlalchemy import *
-# SQLite is not as reliable/demanding as postgres but you can use it
-engine = create_engine('postgres://tester:pass@localhost/vdmtest')
-
-from demo_meta import Session, metadata, init_with_engine
-init_with_engine(engine)
-
-# import the versioned domain model package
-import vdm.sqlalchemy
-
-## -----------------------------
-## Our Tables
-
-# NB: you really need to set up your tables and domain object separately for
-# vdm
-
-wikipage = Table('wikipage', metadata,
-    Column('id', Integer, primary_key=True),
-    Column('name', Unicode(200)),
-    Column('body', UnicodeText),
-    )
-
-# -----------------------------
-# VDM stuff
-
-# Now we need some standard VDM tables
-state_table = vdm.sqlalchemy.make_state_table(metadata)
-revision_table = vdm.sqlalchemy.make_revision_table(metadata)
-
-
-# Make our original table vdm-ready by adding state to it
-vdm.sqlalchemy.make_table_stateful(wikipage)
-# And create the table for the wikipage revisions/versions 
-wikipage_revision = vdm.sqlalchemy.make_table_revisioned(wikipage)
-
-
-## ------------------------------
-## Our Domain Objects
-
-# Suppose your class started out as 
-# Class WikiPage(object):
-#    pass
-#
-# then to make it versioned you just need to add in some Mixins
-
-class WikiPage(
-    vdm.sqlalchemy.StatefulObjectMixin, # make it state aware
-    vdm.sqlalchemy.RevisionedObjectMixin, # make it versioned aware
-    vdm.sqlalchemy.SQLAlchemyMixin # this is optional (provides nice __str__)
-    ):
-
-    pass
-
-
-## Let's map the tables to the domain objects
-mapper = Session.mapper
-
-# VDM-specific domain objects
-State = vdm.sqlalchemy.make_State(mapper, state_table)
-Revision = vdm.sqlalchemy.make_Revision(mapper, revision_table)
-
-
-# Now our domain object.
-# This is just like any standard sqlalchemy mapper setup
-# The only addition is  the mapper extension
-mapper(WikiPage, wikipage, properties={
-    },
-    # mapper extension which handles automatically versioning the object
-    extension=vdm.sqlalchemy.Revisioner(wikipage_revision)
-    )
-# add the revision and state attributes into WikiPage
-vdm.sqlalchemy.modify_base_object_mapper(WikiPage, Revision, State)
-
-# Last: create domain object corresponding to the Revision/Version of the main
-# object
-WikiPageRevision = vdm.sqlalchemy.create_object_version(
-        mapper,
-        WikiPage,
-        wikipage_revision
-        )
-
-# We recommend you use the Repository object to manage your versioned domain
-# objects
-# This isn't required but it provides extra useful features such as purging
-# See the module for full details
-from vdm.sqlalchemy.tools import Repository
-repo = Repository(metadata, Session, versioned_objects=[WikiPage])
-
-
-# Let's try it out
-def test_it():
-    # clean out the db so we start clean
-    repo.rebuild_db()
-
-    # you need to set up a Revision for versioned objects to use
-    # this is set on the SQLAlchemy session so as to be available generally
-    # It can be set up any time before you commit but it is usually best to do
-    # it before you start creating or modifying versioned objects
-
-    # You can set up the revision directly e.g.
-    # rev = Revision()
-    # SQLAlchemySession.set_revision(rev)
-    # (or even just Session.revision = rev)
-    # However this will do the same and is simpler
-    rev = repo.new_revision()
-
-    # now make some changes
-    mypage = WikiPage(name=u'Home', body=u'Some text')
-    mypage2 = WikiPage(name=u'MyPage', body=u'')
-    # let's add a log message to these changes
-    rev.message = u'My first revision'
-    # Just encapsulates Session.commit() + Session.remove()
-    # (with some try/excepts)
-    repo.commit_and_remove()
-
-    last_revision = repo.youngest_revision()
-    assert last_revision.message == u'My first revision'
-    outpage = WikiPage.query.filter_by(name=u'Home').first()
-    assert outpage and outpage.body == u'Some text'
-
-    # let's make some more changes
-

vdm/sqlalchemy/sqla.py

 '''Generic sqlalchemy code (not specifically related to vdm).
 '''
+import uuid
 import sqlalchemy
 
+make_uuid = lambda: unicode(uuid.uuid4())
+
 class SQLAlchemyMixin(object):
     def __init__(self, **kw):
         for k, v in kw.iteritems():
     def __repr__(self):
         return self.__str__()
 
-## --------------------------------------------------------
-## Table Helpers
 
-def copy_column(name, src_table, dest_table):
+class SQLAlchemySession(object):
+    '''Handle setting/getting attributes on the SQLAlchemy session.
+    
+    TODO: update all methods so they can take an object as well as session
+    object.
     '''
-    Note you cannot just copy columns standalone e.g.
 
-        col = table.c['xyz']
-        col.copy()
+    @classmethod
+    def setattr(self, session, attr, value):
+        setattr(session, attr, value)
+        # check if we are being given the Session class (threadlocal case)
+        # if so set on both class and instance
+        # this is important because sqlalchemy's object_session (used below) seems
+        # to return a Session() not Session
+        if isinstance(session, sqlalchemy.orm.scoping.ScopedSession):
+            sess = session()
+            setattr(sess, attr, value)
 
-    This will only copy basic info while more complex properties (such as fks,
-    constraints) to work must be set when the Column has a parent table.
+    @classmethod
+    def getattr(self, session, attr):
+        return getattr(session, attr)
 
-    TODO: stuff other than fks (e.g. constraints such as uniqueness)
-    '''
-    col = src_table.c[name]
-    if col.unique == True:
-        # don't copy across unique constraints, as different versions
-        # of an object may have identical column values
-        col.unique = False
-    dest_table.append_column(col.copy())
-    # only get it once we have a parent table
-    newcol = dest_table.c[name]
-    if len(col.foreign_keys) > 0:
-        for fk in col.foreign_keys: 
-            newcol.append_foreign_key(fk.copy())
+    # make explicit to avoid errors from typos (no attribute defns in python!)
+    @classmethod
+    def set_revision(self, session, revision):
+        self.setattr(session, 'HEAD', True)
+        self.setattr(session, 'revision', revision)
+        if revision.id is None:
+            # make uuid here so that if other objects in this session are flushed
+            # at the same time they know thier revision id
+            revision.id = make_uuid()
+            # there was a begin_nested here but that just caused flush anyway.
+            session.add(revision)
+            session.flush()
 
-def copy_table_columns(table):
-    columns = []
-    for col in table.c:
-        newcol = col.copy() 
-        if len(col.foreign_keys) > 0:
-            for fk in col.foreign_keys: 
-                newcol.foreign_keys.add(fk.copy())
-        columns.append(newcol)
-    return columns
+    @classmethod
+    def get_revision(self, session):
+        '''Get revision on current Session/session.
+        
+        NB: will return None if not set
+        '''
+        return getattr(session, 'revision', None)
 
-def copy_table(table, newtable):
-    for key in table.c.keys():
-        copy_column(key, table, newtable)
+    @classmethod
+    def set_not_at_HEAD(self, session):
+        self.setattr(session, 'HEAD', False)
 
+    @classmethod
+    def at_HEAD(self, session):
+        return getattr(session, 'HEAD', True)
+

vdm/sqlalchemy/stateful.py

-'''Support for stateful collections.
-
-Stateful collections are essential to the functioning of VDM's m2m support.
-m2m relationships become stateful when versioned and the associated collection must
-become state-aware.
-
-There are several subtleties in doing this, the most significant of which is
-how one copes with adding an "existing" object to a stateful list which already
-contains that object in deleted form (or, similarly, when moving an item within
-a list which normally corresponds to a delete and an insert).
-
-
-Stateful Lists and "Existing" Objects Problem
-=============================================
-
-The problem here is that the "existing" object and the object being added are
-not literally the same object in the python sense. Why? First, because their
-state may differ and the ORM is not aware that state is irrelevant to identity.
-Second, and more significantly because the ORM often does not fully "create"
-the object until a flush which is too late - we already have duplicates in the
-list. Here's a concrete example::
-
-    # Package, Tag, PackageTag objects with Package.package_tags_active being
-    # StatefulList and Package.tags a proxy to this showing the tags
-
-    pkg.tags = [ tag1 ]
-    # pkg.package_tags now contains 2 items
-    # PackageTag(pkg=
-
-    pkg1 = Package('abc')
-    tag1 = Tag(name='x')
-    pkg1.tags.append(tag1)
-    # pkg1.package_tags contains one PackageTag
-
-    # delete tag1
-    del pkg.tags[0]
-    # so PackageTag(pkg=pkg1, tag=tag1) is now in deleted state
-    pkg.tags.append(tag1)
-    # now pkg.package_tags has length 2!
-    # Why? Really we want to undelete PackageTag(pkg=pkg1, tag=tag1)
-    # however for association proxy what happens is that
-    # we get a new PackageTag(pkg=None, tag=tag1) created and this is not
-    # identified with existing PackageTag(pkg=pkg1, tag=tag1) because pkg=None
-    # on new PackageTag (pkg only set on flush)
-    # Thus a new item is appended rather than existing being undeleted
-
-    # even more seriously suppose pkg.tags is [tag1]
-    # what happens if we do
-    pkg.tags = [tag1]
-    # does *not* result in nothing happen
-    # instead existing PackageTag(pkg=pkg1, tag=tag1) is put in deleted state and
-    # new PackageTag(pkg=None, tag=tag1) is appended with this being changed to
-    # PackageTag(pkg=pkg1, tag=tag1) on commit (remember sqlalchemy does not
-    # resolve m2m objects foreign key for owner object until flush time)
-
-How do we solve this? The only real answer is implement an identity map in the
-stateful list based on a supplier identifier function. This is done in the code
-below. It has the effect of restoring expected behaviour in the above examples
-(further demonstrations can be found in the tests).
-
-
-TODO: create some proper tests for base_modifier stuff.
-TODO: move stateful material from base.py here?
-'''
-import logging
-logger = logging.getLogger('vdm.stateful')
-
-import itertools
-
-
-class StatefulProxy(object):
-    '''A proxy to an underlying collection which contains stateful objects.
-
-    The proxy only shows objects in a particular state (e.g. active ones) and
-    will also transform standard collection operations to make them 'stateful'
-    -- for example deleting from the list will not delete the object but simply
-    place it in a deleted state.
-    '''
-    def __init__(self, target, **kwargs):
-        '''
-        @param target: the target (underlying) collection (list, dict, etc)
-
-        Possible kwargs:
-        
-        is_active, delete, undelete: a method performing the relevant operation
-            on the underlying stateful objects. If these are not provided they
-            will be created on the basis that there is a corresponding method
-            on the stateful object (e.g. one can do obj.is_active()
-            obj.delete() etc.
-
-        base_modifier: function to operate on base objects before any
-            processing. e.g. could have function:
-
-            def get_as_of(x):
-                return x.get_as_of(revision)
-
-            WARNING: if base_modifier is not trivial (i.e. does not equal the
-            identity function: lambda x: x) then only read operations should be
-            performed on this proxy (this is because you will be operating on
-            modified rather than original objects).
-
-            WARNING: when using base_modifier the objects returned from this list will
-            not be list objects themselves but base_modifier(object).
-            In particular, this means that when base_modifier is turned on
-            operations that change the list (e.g. deletions) will operate on
-            modified objects not the base objects!!
-        '''
-        self.target = target
-
-        extra_args = ['is_active', 'delete', 'undelete', 'base_modifier']
-        for argname in extra_args:
-            setattr(self, argname, kwargs.get(argname, None))
-        if self.is_active is None:
-            # object may not exist (e.g. with get_as_of in which case it will
-            # be None
-            self.is_active = lambda x: not(x is None) and x.is_active()
-        if self.delete is None:
-            self.delete = lambda x: x.delete()
-        if self.undelete is None:
-            self.undelete = lambda x: x.undelete()
-        if self.base_modifier is None:
-            self.base_modifier = lambda x: x
-        self._set_stateful_operators()
-
-    def _set_stateful_operators(self):
-        self._is_active = lambda x: self.is_active(self.base_modifier(x))
-        self._delete = self.delete
-        self._undelete = self.undelete
-
-
-class StatefulList(StatefulProxy):
-    '''A list which is 'state' aware.
-    
-    NB: there is some subtlety as to behaviour when adding an "existing" object
-    to the list -- see the main module docstring for details.
-
-    # TODO: should we have self.base_modifier(obj) more frequently used? (e.g.
-    # in __iter__, append, etc
-    '''
-    def __init__(self, target, **kwargs):
-        '''Same as for StatefulProxy but with additional kwarg:
-
-        @param identifier: a function which takes an object and return a key
-            identifying that object for use in an internal identity map. (See
-            discussion in main docstring for why this is required).
-
-        @param unneeded_deleter: function to delete objects which have
-        been added to the list but turn out to be unneeded (because existing
-        deleted object already exists). Reason for existence: (going back to
-        example in main docstring) suppose we pkg1.package_tags already
-        contains tag1 ('geo') but in deleted state. We then do
-        pkg1.tags.append(tag1). This results in a new PackageTag pkgtag2 being
-        created by creator function in assoc proxy and passed on to stateful
-        list. Thanks to the identifier we notice this already exists and
-        undelete the existing PackageTag rather than adding this new one. But
-        what do we with this pkgtag2? We need to 'get rid of it' so it is not
-        committed into the the db.
-        '''
-        super(StatefulList, self).__init__(target, **kwargs)
-        identifier = kwargs.get('identifier', lambda x: x)
-        unneeded_deleter = kwargs.get('unneeded_deleter', lambda x: None)
-        self._identifier = identifier
-        self._unneeded_deleter = unneeded_deleter
-        self._identity_map = {}
-        for obj in self.target:
-            self._add_to_identity_map(obj)
-
-    def _get_base_index(self, idx):
-        # if we knew items were unique could do
-        # return self.target.index(self[myindex])
-        count = -1
-        basecount = -1
-        if idx < 0:
-            myindex = -(idx) - 1
-            tbaselist = reversed(self.target)
-        else:
-            myindex = idx
-            tbaselist = self.target
-        for item in tbaselist:
-            basecount += 1
-            if self._is_active(item):
-                count += 1
-            if count == myindex:
-                if idx < 0:
-                    return -(basecount + 1)
-                else:
-                    return basecount
-        raise IndexError
-
-    def _add_to_identity_map(self, obj):
-        objkey = self._identifier(obj)
-        current = self._identity_map.get(objkey, [])
-        current.append(obj)
-        self._identity_map[objkey] = current
-
-    def _existing_deleted_obj(self, objkey):
-        for existing_obj in self._identity_map.get(objkey, []):
-            if not self._is_active(existing_obj): # return 1st we find
-                return existing_obj
-
-    def _check_for_existing_on_add(self, obj):
-        objkey = self._identifier(obj)
-        out_obj = self._existing_deleted_obj(objkey)
-        if out_obj is None: # no existing deleted object in list
-            out_obj = obj 
-            self._add_to_identity_map(out_obj)
-        else: # deleted object already in list
-            # we are about to re-add (in active state) so must remove it first
-            idx = self.target.index(out_obj)
-            del self.target[idx]
-            # We now have have to deal with original `obj` that was passed in
-            self._unneeded_deleter(obj)
-
-        self._undelete(out_obj)
-        return out_obj
-
-    def append(self, in_obj):
-        obj = self._check_for_existing_on_add(in_obj)
-        self.target.append(obj)
-
-    def insert(self, index, value):
-        # have some choice here so just for go for first place
-        our_obj = self._check_for_existing_on_add(value)
-        try:
-            baseindex = self._get_base_index(index)
-        except IndexError: # may be list is empty ...
-            baseindex = len(self)
-        self.target.insert(baseindex, our_obj)
-
-    def __getitem__(self, index):
-        baseindex = self._get_base_index(index)
-        return self.base_modifier(self.target[baseindex])
-    
-    def __item__(self, index):
-        return self.__getitem__(self, index)
-    
-    def __delitem__(self, index):
-        if not isinstance(index, slice):
-            self._delete(self[index])
-        else:
-            start = index.start
-            end = index.stop
-            rng = range(start, end)
-            for ii in rng:
-                del self[start]
-
-    def __setitem__(self, index, value):
-        if not isinstance(index, slice):
-            del self[index]
-            self.insert(index, value)
-        else:
-            if index.stop is None:
-                stop = len(self)
-            elif index.stop < 0:
-                stop = len(self) + index.stop
-            # avoid weird MemoryError when doing OurList[:] = ...
-            elif index.stop > len(self):
-                stop = len(self)
-            else:
-                stop = index.stop
-            step = index.step or 1
-
-            rng = range(index.start or 0, stop, step)
-            if step == 1:
-                # delete first then insert to avoid problems with indices and
-                # statefulness
-                for ii in rng:
-                    start = rng[0]
-                    del self[start]
-                ii = index.start or 0
-                for item in value:
-                    self.insert(ii, item)
-                    ii += 1
-            else:
-                if len(value) != len(rng):
-                    raise ValueError(
-                                'attempt to assign sequence of size %s to '
-                                'extended slice of size %s' % (len(value),
-                                                               len(rng)))
-                    for ii, item in zip(rng, value):
-                        self[ii] = item
-
-    # def __setslice__(self, start, end, values):
-    #    for ii in range(start, end):
-    #        self[ii] = values[ii-start]
-
-    def __iter__(self):
-        mytest = lambda x: self._is_active(x)
-        myiter = itertools.ifilter(mytest, iter(self.target))
-        return myiter
-    
-    def __len__(self):
-        return sum([1 for _ in self])
-
-    def count(self, item):
-        myiter = itertools.ifilter(lambda v: v == item, iter(self))
-        counter = [1 for _ in myiter]
-        return sum(counter)
-
-    def extend(self, values):
-        for val in values:
-            self.append(val)
-
-    def copy(self):
-        return list(self)
-
-    def clear(self):
-        del self[0:len(self)]
-    
-    def pop(self, index=None):
-        raise NotImplementedError
-
-    def reverse(self):
-        raise NotImplementedError
-    
-    def __repr__(self):
-        return repr(self.target)
-
-
-class StatefulListDeleted(StatefulList):
-
-    def _set_stateful_operators(self):
-        self._is_active = lambda x: not self.is_active(self.base_modifier(x))
-        self._delete = self.undelete
-        self._undelete = self.delete
-
-    
-class StatefulDict(StatefulProxy):
-    '''A stateful dictionary which only shows object in underlying dictionary
-    which are in active state.
-    '''
-
-    # sqlalchemy assoc proxy fails to guess this is a dictionary w/o prompting
-    # (util.duck_type_collection appears to identify dict by looking for a set
-    # method but dicts don't have this method!)
-    __emulates__ = dict
-
-    def __contains__(self, k):
-        return k in self.target and self._is_active(self.target[k])
-
-    def __delitem__(self, k):
-        # will raise KeyError if not there (which is what we want)
-        val = self.target[k]
-        if self._is_active(val):
-            self._delete(val)
-        else:
-            raise KeyError(k)
-        # should we raise KeyError if already deleted?
-
-    def __getitem__(self, k):
-        out = self.target[k]
-        if self._is_active(out):
-            return self.base_modifier(out)
-        else:
-            raise KeyError(k)
-
-    def __iter__(self):
-        myiter = itertools.ifilter(lambda x: self._is_active(self.target[x]),
-                iter(self.target))
-        return myiter
-
-    def __setitem__(self, k, v):
-        self.target[k] = v
-
-    def __len__(self):
-        return sum([1 for _ in self])
-
-    def clear(self): 
-        for k in self:
-            del self[k]
-
-    def copy(self):
-        # return self.__class__(self.target, base_modifier=self.base_modifier)
-        return dict(self)
-
-    def get(self, k, d=None):
-        if k in self:
-            return self[k]
-        else:
-            return d
-
-    def has_key(self, k):
-        return k in self
-    
-    def items(self):
-        return [ x for x in self.iteritems() ]
-
-    def iteritems(self):
-        for k in self:
-            yield k,self[k]
-
-    def keys(self):
-        return [ k for k in self ]
-
-    def iterkeys(self):
-        for k in self:
-            yield k
-
-    def __repr__(self):
-        return repr(self.target)
-
-
-
-class DeferredProperty(object):
-    def __init__(self, target_collection_name, stateful_class, **kwargs):
-        '''Turn StatefulList into a property to allowed for deferred access
-        (important as collections to which they are proxying may also be
-        deferred).
-
-        @param target_collection_name: name of attribute on object to which
-        instance of stateful_class is being attached which stateful_class will
-        'wrap'.
-        @param kwargs: additional arguments to stateful_class (if any)
-
-        For details of other args see L{StatefulList}.
-        '''
-        self.target_collection_name = target_collection_name
-        self.stateful_class = stateful_class
-        self.cached_kwargs = kwargs
-        self.cached_instance_key = '_%s_%s_%s' % (type(self).__name__,
-                self.target_collection_name, id(self))
-
-    def __get__(self, obj, class_):
-        try:
-            # return cached instance
-            return getattr(obj, self.cached_instance_key)
-        except AttributeError:
-            # probably should do this using lazy_collections a la assoc proxy
-            target_collection = getattr(obj, self.target_collection_name)
-            stateful_list = self.stateful_class(target_collection, **self.cached_kwargs)
-            # cache
-            setattr(obj, self.cached_instance_key, stateful_list)
-            return stateful_list
-
-    def __set__(self, obj, values):
-        # Must not replace the StatefulList object with a list,
-        # so instead replace the values in the Stateful list with
-        # the list values passed on. The existing values are accessed
-        # using [:] invoking __setitem__.
-        self.__get__(obj, None)[:] = values
-        
-
-from sqlalchemy import __version__ as sqla_version
-import sqlalchemy.ext.associationproxy
-import weakref
-# write our own assoc proxy which excludes scalar support and therefore calls
-# which will not work since underlying list is a StatefuList not a normal
-# collection 
-class OurAssociationProxy(sqlalchemy.ext.associationproxy.AssociationProxy):
-
-    def _target_is_scalar(self):
-        return False
-
-
-# TODO: 2009-07-24 support dict collections
-def add_stateful_m2m(object_to_alter, m2m_object, m2m_property_name,
-        attr, basic_m2m_name, **kwargs):
-    '''Attach active and deleted stateful lists along with the association
-    proxy based on the active list to original object (object_to_alter).
-
-    To illustrate if one has::
-
-        class Package(object):
-
-            # package_licenses is the basic_m2m_name attribute
-            # 
-            # it should come from a simple relation pointing to PackageLicense
-            # and returns PackageLicense objects (so do *not* use secondary
-            # keyword)
-            #
-            # It will usually not be defined here but in the Package mapper:
-            #
-            # 'package_licenses':relation(License) ...
-            
-            package_licenses = ... from_mapper ...
-
-    Then after running::
-
-        add_stateful_m2m(Package, PackageLicense, 'licenses', 'license',
-        'package_licenses')
-    
-    there will be additional properties:
-
-        # NB: licenses_active and licenses_deleted are lists of PackageLicense
-        # objects while licenses (being an assoc proxy) is a list of Licenses
-        licenses_active # these are active PackageLicenses
-        licenses_deleted # these are deleted PackageLicenses
-        licenses # these are active *Licenses*
-
-    @param attr: the name of the attribute on the Join object corresponding to
-        the target (e.g. in this case 'license' on PackageLicense).
-    @arg **kwargs: these are passed on to the DeferredProperty.
-    '''
-    active_name = m2m_property_name + '_active'
-    # in the join object (e.g. PackageLicense) the License object accessible by
-    # the license attribute will be what we need for our identity map
-    if not 'identifier' in kwargs:
-        def identifier(joinobj):
-            return getattr(joinobj, attr)
-        kwargs['identifier'] = identifier
-    if not 'unneeded_deleter' in kwargs:
-        # For sqlalchemy this means expunging the object ...
-        # (Note we can assume object is 'new' since it didn't match any
-        # existing one in -- see _check_for_existing_on_add in StatefulList)
-        # (NB: this assumption turns out to be false in some special cases --
-        # see comments in test in test_demo.py:TestVersioning2.test_2)
-        from sqlalchemy.orm import object_session
-        def _f(obj_to_delete):
-            sess = object_session(obj_to_delete)
-            if sess: # for tests at least must support obj not being sqlalchemy
-                sess.expunge(obj_to_delete)
-        kwargs['unneeded_deleter'] = _f
-
-    active_prop = DeferredProperty(basic_m2m_name, StatefulList, **kwargs)
-    deleted_name = m2m_property_name + '_deleted'
-    deleted_prop = DeferredProperty(basic_m2m_name, StatefulListDeleted,
-            **kwargs)
-    setattr(object_to_alter, active_name, active_prop)
-    setattr(object_to_alter, deleted_name, deleted_prop)
-    create_m2m = make_m2m_creator_for_assocproxy(m2m_object, attr)
-    setattr(object_to_alter, m2m_property_name,
-            OurAssociationProxy(active_name, attr, creator=create_m2m)
-            )
-
-
-def make_m2m_creator_for_assocproxy(m2m_object, attrname):
-    '''This creates a_creator function for SQLAlchemy associationproxy pattern.
-
-    @param m2m_object: the m2m object underlying association proxy.
-    @param attrname: the attrname to use for the default object passed in to m2m
-    '''
-    def create_m2m(foreign, **kw):
-        mykwargs = dict(kw)
-        mykwargs[attrname] = foreign
-        return m2m_object(**mykwargs)
-
-    return create_m2m

vdm/sqlalchemy/test_tools.py

-# many of the tests are in demo_test as that sets up nice fixtures
-from tools import *
-
-dburi = 'postgres://tester:pass@localhost/vdmtest'
-from demo import *
-class TestRepository:
-    repo = Repository(metadata, Session, dburi)
-
-    def test_transactional(self):
-        assert self.repo.have_scoped_session
-        assert self.repo.transactional
-
-    def test_init_vdm(self):
-        self.repo.session.remove()
-        self.repo.clean_db()
-        self.repo.create_db()
-        self.repo.init_db()
-        # nothing to test at the moment ...
-
-    def test_new_revision(self):
-        self.repo.session.remove()
-        rev = self.repo.new_revision()
-        assert rev is not None
-
-    def test_history(self):
-        self.repo.session.remove()
-        self.repo.rebuild_db()
-        rev = self.repo.new_revision()
-        rev.message = u'abc'
-        self.repo.commit_and_remove()
-        history = self.repo.history()
-        revs = history.all()
-        assert len(revs) == 1
-

vdm/sqlalchemy/tools.py

 from sqlalchemy.orm import object_session
 from sqlalchemy import __version__ as sqla_version
 
-from .base import SQLAlchemySession
+from .sqla import SQLAlchemySession
 from .changeset import Changeset
 
 class Repository(object):
         
         @return: sqlalchemy query object.
         '''
-        return self.session.query(Changeset).filter_by(state=State.ACTIVE)
+        return self.session.query(Changeset)
 
     def list_changes(self, revision):
         '''List all objects changed by this `revision`.

vdm/test/sqlalchemy/test_tools.py

+# many of the tests are in demo_test as that sets up nice fixtures
+from vdm.sqlalchemy.tools import *
+
+dburi = 'postgres://tester:pass@localhost/vdmtest'
+from .demo import *
+
+class TestRepository:
+    repo = Repository(metadata, Session, dburi)
+
+    def test_transactional(self):
+        assert self.repo.have_scoped_session
+        assert self.repo.transactional
+
+    def test_init_vdm(self):
+        self.repo.session.remove()
+        self.repo.clean_db()
+        self.repo.create_db()
+        self.repo.init_db()
+        # nothing to test at the moment ...
+
+    def test_new_revision(self):
+        self.repo.session.remove()
+        rev = self.repo.new_revision()
+        assert rev is not None
+
+    def test_history(self):
+        self.repo.session.remove()
+        self.repo.rebuild_db()
+        rev = self.repo.new_revision()
+        rev.message = u'abc'
+        self.repo.commit_and_remove()
+        history = self.repo.history()
+        revs = history.all()
+        assert len(revs) == 1
+