[Checkins] SVN: z3c.zalchemy/trunk/ Merging changes from `ctheune-automatic-session` branch.

Christian Theune ct at gocept.com
Thu Jun 28 02:03:10 EDT 2007


Log message for revision 77165:
  Merging changes from `ctheune-automatic-session` branch.
  

Changed:
  U   z3c.zalchemy/trunk/CHANGES.txt
  U   z3c.zalchemy/trunk/setup.py
  U   z3c.zalchemy/trunk/src/z3c/zalchemy/README.txt
  U   z3c.zalchemy/trunk/src/z3c/zalchemy/datamanager.py
  U   z3c.zalchemy/trunk/src/z3c/zalchemy/metaconfigure.py
  U   z3c.zalchemy/trunk/src/z3c/zalchemy/tests/TRANSACTION.txt
  U   z3c.zalchemy/trunk/src/z3c/zalchemy/tests/test_directives.py
  U   z3c.zalchemy/trunk/src/z3c/zalchemy/tests/test_zalchemy.py

-=-
Modified: z3c.zalchemy/trunk/CHANGES.txt
===================================================================
--- z3c.zalchemy/trunk/CHANGES.txt	2007-06-28 06:01:13 UTC (rev 77164)
+++ z3c.zalchemy/trunk/CHANGES.txt	2007-06-28 06:03:09 UTC (rev 77165)
@@ -5,7 +5,16 @@
 0.2 - unreleased
 ================
 
+  - Provide a tighter integration with Zope's transaction mechanism. Sessions
+    are now automatically associated with new objects. We rely on SQLAlchemy's
+    SessionContext object which hands out a session for each thread. Your code
+    rarely should never have to call `session.save(object)` now.
 
+    One incompatible change was introduced: You can not call `getSession`
+    before registering an (unnamed) engine utility first. Doing so will raise
+    a ValueError.
+
+
 0.1.1 - 2007-06-27
 ==================
 

Modified: z3c.zalchemy/trunk/setup.py
===================================================================
--- z3c.zalchemy/trunk/setup.py	2007-06-28 06:01:13 UTC (rev 77164)
+++ z3c.zalchemy/trunk/setup.py	2007-06-28 06:03:09 UTC (rev 77165)
@@ -20,7 +20,7 @@
                         'zope.schema',
                         'zope.app.testing',
                         'zope.app.component',
-                        'zope.app.keyreference<3.5dev',
+                        'zope.app.keyreference',
                         'zope.app.container',
                         'zope.app.pagetemplate',
                        ],

Modified: z3c.zalchemy/trunk/src/z3c/zalchemy/README.txt
===================================================================
--- z3c.zalchemy/trunk/src/z3c/zalchemy/README.txt	2007-06-28 06:01:13 UTC (rev 77164)
+++ z3c.zalchemy/trunk/src/z3c/zalchemy/README.txt	2007-06-28 06:03:09 UTC (rev 77165)
@@ -92,50 +92,29 @@
   >>> provideUtility(engineUtility, IAlchemyEngineUtility)
 
 Tables can be created without an open transaction or session.
-If no session is created then the table creation is deffered to the next
+If no session is created then the table creation is deferred to the next
 call to zalchemy.getSession.
 
   >>> z3c.zalchemy.createTable('table3', '')
 
-Note that the transaction handling is done inside Zope.
+zalchemy automatically coordinates Zope's transaction manager with
+SQLAlchemy's sessions. All mapped classes are automatically associated with
+thread-local session, which in turn is automatically connected to a special
+data manager that coordinates with Zope's transactions.
 
-  >>> import transaction
-  >>> txn = transaction.begin()
-
-Everything inside SQLAlchemy needs a Session. We must obtain the Session
-from zalchemy. This makes sure that a transaction handler is inserted into
-Zope's transaction process.
-
-To simplify the usage of getSession we store the function in "session" (see
-also the note above).
-
-  >>> session = z3c.zalchemy.getSession
-
   >>> a = A()
   >>> a.value = 1
 
-Apply the new object to the session :
+Committing a transaction will automatically trigger a flush and clear the
+session.
 
-  >>> session().save(a)
-
-A new instance of a mapped sqlobject class is created. This object is not
-stored in the database until the session is committed or flush is called for
-the new instance.
-
-To be able to query a new instance it is therefore necessary to flush the
-object to the database before the query.
-
-  >>> session().flush([a])
-
-Commiting a transaction is doing the same with all remaining instances.
-After this commit the current session is flushed and cleared.
-
+  >>> import transaction
   >>> transaction.commit()
 
-Now let's try to get the object back in a new transaction :
+Now let's try to get the object back in a new transaction (we're in a new
+transaction already because the old transaction was committed):
 
-  >>> txn = transaction.begin()
-
+  >>> from z3c.zalchemy.datamanager import getSession as session
   >>> a = session().get(A, 1)
   >>> a.value
   1
@@ -146,9 +125,9 @@
 Multiple databases
 ------------------
 
-The above example asumed that there is only one database.
-The database engine was registered as unnamed utility.
-The unnamed utility is always the default database for new sessions.
+The above example assumed that there is only one database.  The database
+engine was registered as an unnamed utility.  The unnamed utility is always
+the default database for new sessions.
 
 This automatically assigns every table to the default engine.
 
@@ -179,8 +158,6 @@
   ...     pass
   >>> B.mapper = sqlalchemy.mapper(B, bTable)
 
-  >>> txn = transaction.begin()
-
 Assign bTable to the new engine and create the table.
 This time we do it inside of a session.
 
@@ -188,17 +165,13 @@
   >>> z3c.zalchemy.createTable('bTable', 'engine2')
 
   >>> b = B()
-  >>> session().save(b)
   >>> b.value = 'b1'
 
   >>> a = A()
-  >>> session().save(a)
   >>> a.value = 321
 
   >>> transaction.commit()
 
-  >>> txn = transaction.begin()
-
   >>> a = session().get(A, 1)
   >>> b = session().get(B, 1)
   >>> str(b.value)
@@ -222,10 +195,7 @@
 
   >>> z3c.zalchemy.createTable('table3', 'engine2')
 
-  >>> txn = transaction.begin()
-
   >>> aa = Aa()
-  >>> session().save(aa)
   >>> aa.value = 100
 
   >>> transaction.commit()

Modified: z3c.zalchemy/trunk/src/z3c/zalchemy/datamanager.py
===================================================================
--- z3c.zalchemy/trunk/src/z3c/zalchemy/datamanager.py	2007-06-28 06:01:13 UTC (rev 77164)
+++ z3c.zalchemy/trunk/src/z3c/zalchemy/datamanager.py	2007-06-28 06:03:09 UTC (rev 77165)
@@ -25,8 +25,10 @@
 from interfaces import IAlchemyEngineUtility
 
 import sqlalchemy
+from sqlalchemy.orm.mapper import global_extensions
+from sqlalchemy.ext.sessioncontext import SessionContext
+from sqlalchemy.orm.session import Session
 
-
 class AlchemyEngineUtility(persistent.Persistent):
     """A utility providing a database engine.
     """
@@ -75,50 +77,77 @@
 _tableToEngine = {}
 _classToEngine = {}
 _tablesToCreate = []
-_storage = local()
 
-def getSession(createTransaction=False):
-    session=getattr(_storage,'session',None)
-    if session:
-        return session
-    txn = transaction.manager.get()
-    if createTransaction and (txn is None):
-        txn = transaction.begin()
+# SQLAlchemy session management through thread-locals and our own data
+# manager.
+
+def createSession():
+    """Creates a new session that is bound to the default engine utility and
+    hooked up with the Zope transaction machinery.
+
+    """
     util = queryUtility(IAlchemyEngineUtility)
-    engine = None
-    if util is not None:
-        engine = util.getEngine()
-    _storage.session=sqlalchemy.create_session(bind_to=engine)
-    session = _storage.session
-    for table, engine in _tableToEngine.iteritems():
-        _assignTable(table, engine)
-    for class_, engine in _classToEngine.iteritems():
-        _assignClass(class_, engine)
-    if txn is not None:
-        _storage.dataManager = AlchemyDataManager(session)
-        txn.join(_storage.dataManager)
-    _createTables()
+    if util is None:
+        raise ValueError("No engine utility registered")
+    engine = util.getEngine()
+    session = sqlalchemy.create_session(bind_to=engine)
+
+    # This session is now only bound to the default engine. We need to bind
+    # the other explicitly bound tables and classes as well.
+    bind_session(session)
+
+    transaction.get().join(AlchemyDataManager(session))
     return session
 
 
+def bind_session(session):
+    """Applies all table and class bindings to the given session."""
+    for table, engine in _tableToEngine.items():
+        _assignTable(table, engine, session)
+    for class_, engine in _classToEngine.items():
+        _assignClass(class_, engine, session)
+
+
+ctx = SessionContext(createSession)
+global_extensions.append(ctx.mapper_extension)
+
+
+def getSession():
+    return ctx.current
+
+
 def getEngineForTable(t):
     name = _tableToEngine[t]
     util = getUtility(IAlchemyEngineUtility, name=name)
     return util.getEngine()
-    
 
+
 def inSession():
-    return getattr(_storage,'session',None) is not None
+    return True
 
 
-def assignTable(table, engine):
+def assignTable(table, engine, immediate=True):
+    """Assign a table to an engine and propagate the binding to the current
+    session.
+
+    The binding is not applied to the current session if `immediate` is False.
+
+    """
     _tableToEngine[table]=engine
-    _assignTable(table, engine)
+    if immediate:
+        _assignTable(table, engine)
 
 
-def assignClass(class_, engine):
+def assignClass(class_, engine, immediate=True):
+    """Assign a class to an engine and propagate the binding to the current
+    session.
+
+    The binding is not applied to the current session if `immediate` is False.
+
+    """
     _classToEngine[class_]=engine
-    _assignClass(class_, engine)
+    if immediate:
+        _assignClass(class_, engine)
 
 
 def createTable(table, engine):
@@ -126,26 +155,27 @@
     _createTables()
 
 
-def _assignTable(table, engine):
-    if inSession():
-        t = metadata.getTable(engine, table, True)
-        util = getUtility(IAlchemyEngineUtility, name=engine)
-        _storage.session.bind_table(t,util.getEngine())
+def _assignTable(table, engine, session=None):
+    t = metadata.getTable(engine, table, True)
+    util = getUtility(IAlchemyEngineUtility, name=engine)
+    if session is None:
+            session = ctx.current
+    session.bind_table(t, util.getEngine())
 
 
-def _assignClass(class_, engine):
-    if inSession():
-        m = sqlalchemy.orm.class_mapper(class_)
-        util = getUtility(IAlchemyEngineUtility, name=engine)
-        _storage.session.bind_mapper(m,util.getEngine())
+def _assignClass(class_, engine, session=None):
+    m = sqlalchemy.orm.class_mapper(class_)
+    util = getUtility(IAlchemyEngineUtility, name=engine)
+    if session is None:
+        session = ctx.current
+    session.bind_mapper(m,util.getEngine())
 
 
 def _createTables():
-    if inSession():
-        tables = _tablesToCreate[:]
-        del _tablesToCreate[:]
-        for table, engine in tables:
-            _doCreateTable(table, engine)
+    tables = _tablesToCreate[:]
+    del _tablesToCreate[:]
+    for table, engine in tables:
+        _doCreateTable(table, engine)
 
 
 def _doCreateTable(table, engine):
@@ -166,28 +196,18 @@
         pass
 
 
-def _dataManagerFinished():
-    _storage.session = None
-    _storage.dataManager = None
-    utils = getUtilitiesFor(IAlchemyEngineUtility)
-    for util in utils:
-        util[1]._resetEngine()
+class AlchemyDataManager(object):
+    """Takes care of the transaction process in Zope. """
 
-
-class AlchemyDataManager(object):
-    """Takes care of the transaction process in zope.
-    """
     implements(IDataManager)
 
-    _commitFailed = False
-
     def __init__(self, session):
         self.session = session
         self.transaction = session.create_transaction()
 
     def abort(self, trans):
         self.transaction.rollback()
-        _dataManagerFinished()
+        self._cleanup()
 
     def tpc_begin(self, trans):
         pass
@@ -200,16 +220,23 @@
 
     def tpc_finish(self, trans):
         self.transaction.commit()
-        _dataManagerFinished()
+        self._cleanup()
 
     def tpc_abort(self, trans):
         self.transaction.rollback()
-        _dataManagerFinished()
+        self._cleanup()
 
     def sortKey(self):
         return str(id(self))
 
+    def _cleanup(self):
+        self.session.clear()
+        del ctx.current
+        utils = getUtilitiesFor(IAlchemyEngineUtility)
+        for name, util in utils:
+            util._resetEngine()
 
+
 class MetaManager(object):
     """A manager for metadata to be able to use the same table name in
     different databases.

Modified: z3c.zalchemy/trunk/src/z3c/zalchemy/metaconfigure.py
===================================================================
--- z3c.zalchemy/trunk/src/z3c/zalchemy/metaconfigure.py	2007-06-28 06:01:13 UTC (rev 77164)
+++ z3c.zalchemy/trunk/src/z3c/zalchemy/metaconfigure.py	2007-06-28 06:03:09 UTC (rev 77165)
@@ -19,6 +19,7 @@
 
 import z3c.zalchemy
 
+
 def engine(_context, url, name='', echo=False, **kwargs):
     engine = AlchemyEngineUtility(name, url, echo=echo, **kwargs)
     utility(_context,
@@ -27,12 +28,26 @@
             permission=PublicPermission,
             name=name)
 
+
 def connectTable(_context, table, engine):
-    z3c.zalchemy.assignTable(table, engine)
+    _context.action(
+        discriminator=('zalchemy.table', table),
+        callable=z3c.zalchemy.assignTable,
+        args=(table, engine, False)
+    )
 
+
 def connectClass(_context, class_, engine):
-    z3c.zalchemy.assignClass(class_, engine)
+    _context.action(
+        discriminator=('zalchemy.class', class_),
+        callable=z3c.zalchemy.assignClass,
+        args=(class_, engine, False)
+    )
 
+
 def createTable(_context, table, engine):
-    z3c.zalchemy.createTable(table, engine)
-
+    _context.action(
+        discriminator=('zalchemy.create-table', table),
+        callable=z3c.zalchemy.createTable,
+        args=(table, engine)
+    )

Modified: z3c.zalchemy/trunk/src/z3c/zalchemy/tests/TRANSACTION.txt
===================================================================
--- z3c.zalchemy/trunk/src/z3c/zalchemy/tests/TRANSACTION.txt	2007-06-28 06:01:13 UTC (rev 77164)
+++ z3c.zalchemy/trunk/src/z3c/zalchemy/tests/TRANSACTION.txt	2007-06-28 06:03:09 UTC (rev 77165)
@@ -65,14 +65,11 @@
   >>> transaction.commit()
 
 After the commit we can get a new session from zalchemy outside of a
-transaction.
-We can tell zalchemy to create a new transaction if there is none active.
-But we need to commit the transaction manually.
+transaction.  We can tell zalchemy to create a new transaction if there is
+none active.  But we need to commit the transaction manually.
 
-  >>> session2 = z3c.zalchemy.getSession(True)
-  >>> session == session2
-  False
-  >>> a=A()
+  >>> session2 = z3c.zalchemy.getSession()
+  >>> a = A()
   >>> session2.save(a)
   >>> a.value = 2
   >>> transaction.commit()
@@ -88,7 +85,7 @@
   >>> log = []
   >>> def differentSession():
   ...     global session
-  ...     log.append(('differentSession',session == z3c.zalchemy.getSession()))
+  ...     log.append(('differentSession', session == z3c.zalchemy.getSession()))
   ...
 
   >>> thread = threading.Thread(target=differentSession)
@@ -104,10 +101,9 @@
   ...     txn = transaction.begin()
   ...     session = z3c.zalchemy.getSession()
   ...     obj = session.get(A, 1)
-  ...     obj.value+= 1
+  ...     obj.value += 1
   ...     log.append(('modifyA', obj.value))
   ...     transaction.commit()
-  ...
 
   >>> thread = threading.Thread(target=modifyA)
   >>> thread.start()
@@ -115,7 +111,7 @@
   >>> log
   [('modifyA', 2)]
 
-Nested Threads :
+Nested Threads:
 
   >>> log = []
 
@@ -129,7 +125,6 @@
   ...     obj.value+= 1
   ...     log.append(('nested', obj.value))
   ...     transaction.commit()
-  ...
 
   >>> thread = threading.Thread(target=nested)
   >>> thread.start()
@@ -141,20 +136,20 @@
 Aborting transactions
 ---------------------
 
-  >>> session = z3c.zalchemy.getSession(True)
-  >>> a=session.get(A, 1)
-  >>> v = a.value
+  >>> session = z3c.zalchemy.getSession()
+  >>> a = session.get(A, 1)
+  >>> a.value = 2
+  >>> transaction.commit()
+
   >>> a.value += 1
-  >>> session.flush([a])
+  >>> a.value
+  3
   >>> transaction.abort()
 
-  >>> session = z3c.zalchemy.getSession(True)
-  >>> a=session.get(A, 1)
+  >>> session = z3c.zalchemy.getSession()
+  >>> a = session.get(A, 1)
   >>> a.value
-  3
-  
-  >>> a.value == v
-  True
+  2
 
 
 Two Phase Commit With Errors
@@ -164,7 +159,7 @@
 is called. SQLAlchemy's transaction is commited in the second phase of the
 zope transacion.
 
-  >>> session = z3c.zalchemy.getSession(True)
+  >>> session = z3c.zalchemy.getSession()
   >>> aa=A()
   >>> session.save(aa)
   >>> aa.value = 3
@@ -175,19 +170,18 @@
 
 Let's make sure we get an exception when using commit.
 
-  >>> from z3c.zalchemy.datamanager import _storage
-  >>> _storage.dataManager.commit(transaction.manager.get())
+  >>> transaction.commit()
   Traceback (most recent call last):
   ...
   SQLError: (IntegrityError) PRIMARY KEY must be unique u'INSERT INTO table2 (id, value) VALUES (?, ?)' [2, 3]
 
-Finally we need to do an abort zope's transaction.
+Finally we need to abort zope's transaction.
 
   >>> transaction.abort()
 
 And we do the same using the commit from the transaction.
 
-  >>> session = z3c.zalchemy.getSession(True)
+  >>> session = z3c.zalchemy.getSession()
   >>> aa=A()
   >>> session.save(aa)
   >>> aa.value = 3

Modified: z3c.zalchemy/trunk/src/z3c/zalchemy/tests/test_directives.py
===================================================================
--- z3c.zalchemy/trunk/src/z3c/zalchemy/tests/test_directives.py	2007-06-28 06:01:13 UTC (rev 77164)
+++ z3c.zalchemy/trunk/src/z3c/zalchemy/tests/test_directives.py	2007-06-28 06:03:09 UTC (rev 77165)
@@ -85,7 +85,7 @@
                 />
             '''
             )))
-        util = component.getUtility(IAlchemyEngineUtility,'sqlite-in-memory')
+        util = component.getUtility(IAlchemyEngineUtility, 'sqlite-in-memory')
         self.assert_(len(z3c.zalchemy.datamanager._tableToEngine)==1)
         self.assert_('testTable' in z3c.zalchemy.datamanager._tableToEngine)
         self.assert_(mappedTestClass in z3c.zalchemy.datamanager._classToEngine)

Modified: z3c.zalchemy/trunk/src/z3c/zalchemy/tests/test_zalchemy.py
===================================================================
--- z3c.zalchemy/trunk/src/z3c/zalchemy/tests/test_zalchemy.py	2007-06-28 06:01:13 UTC (rev 77164)
+++ z3c.zalchemy/trunk/src/z3c/zalchemy/tests/test_zalchemy.py	2007-06-28 06:03:09 UTC (rev 77165)
@@ -56,9 +56,9 @@
         z3c.zalchemy.testing.tearDown(self)
 
     def testNoDefaultEngine(self):
-        session = z3c.zalchemy.getSession()
-        self.assertNotEqual(session, None)
-        self.assertEqual(session.get_bind(None), None)
+        # Our session can't work without an engine. If we did not 
+        # register an IAlchemyEngineUtility, we can't access the session
+        self.assertRaises(ValueError, z3c.zalchemy.getSession)
 
     def testDefaultEngine(self):
         from zope.component import provideUtility



More information about the Checkins mailing list