[Checkins] SVN: transaction/trunk/ Merge 'sphinx' branch.

Tres Seaver cvs-admin at zope.org
Tue Dec 18 05:26:55 UTC 2012


Log message for revision 128757:
  Merge 'sphinx' branch.

Changed:
  _U  transaction/trunk/
  U   transaction/trunk/CHANGES.txt
  A   transaction/trunk/docs/convenience.rst
  A   transaction/trunk/docs/datamanager.rst
  A   transaction/trunk/docs/doom.rst
  A   transaction/trunk/docs/hooks.rst
  U   transaction/trunk/docs/index.rst
  A   transaction/trunk/docs/resourcemanager.rst
  A   transaction/trunk/docs/savepoint.rst
  U   transaction/trunk/setup.py
  A   transaction/trunk/transaction/_compat.py
  U   transaction/trunk/transaction/_manager.py
  U   transaction/trunk/transaction/_transaction.py
  D   transaction/trunk/transaction/compat.py
  A   transaction/trunk/transaction/tests/common.py
  D   transaction/trunk/transaction/tests/convenience.txt
  D   transaction/trunk/transaction/tests/doom.txt
  A   transaction/trunk/transaction/tests/examples.py
  D   transaction/trunk/transaction/tests/sampledm.py
  D   transaction/trunk/transaction/tests/savepoint.txt
  U   transaction/trunk/transaction/tests/savepointsample.py
  D   transaction/trunk/transaction/tests/test_SampleDataManager.py
  D   transaction/trunk/transaction/tests/test_SampleResourceManager.py
  A   transaction/trunk/transaction/tests/test__manager.py
  A   transaction/trunk/transaction/tests/test__transaction.py
  D   transaction/trunk/transaction/tests/test_attempt.py
  U   transaction/trunk/transaction/tests/test_register_compat.py
  U   transaction/trunk/transaction/tests/test_savepoint.py
  D   transaction/trunk/transaction/tests/test_transaction.py
  U   transaction/trunk/transaction/tests/test_weakset.py

-=-

Property changes on: transaction/trunk
___________________________________________________________________
Added: svn:mergeinfo
   + /transaction/branches/sphinx:128695-128726,128733-128739,128741-128756

Added: svk:merge
   + 62d5b8a3-27da-0310-9561-8e5933582275:/transaction/branches/sphinx:128756


Modified: transaction/trunk/CHANGES.txt
===================================================================
--- transaction/trunk/CHANGES.txt	2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/CHANGES.txt	2012-12-18 05:26:55 UTC (rev 128757)
@@ -4,6 +4,20 @@
 1.3.1 (unreleased)
 ------------------
 
+- Refactored existing doctests as Sphinx documentation (snippets are exercised
+  via 'tox').
+
+- 100% unit test coverage.
+
+- Raise ValueError from ``Transaction.doom`` if the transaction is in a
+  non-doomable state (rather than using ``assert``).
+
+- Raise ValueError from ``TransactionManager.attempts`` if passed a
+  non-positive value (rather than using ``assert``).
+
+- Raise ValueError from ``TransactionManager.free`` if passed a foreign
+  transaction (rather tna using ``assert``).
+
 - Declared support for Python 3.3 in ``setup.py``, and added ``tox`` testing.
 
 - When a non-retryable exception was raised as the result of a call to

Copied: transaction/trunk/docs/convenience.rst (from rev 128495, transaction/trunk/transaction/tests/convenience.txt)
===================================================================
--- transaction/trunk/docs/convenience.rst	                        (rev 0)
+++ transaction/trunk/docs/convenience.rst	2012-12-18 05:26:55 UTC (rev 128757)
@@ -0,0 +1,180 @@
+Transaction convenience support
+===============================
+
+(We *really* need to write proper documentation for the transaction
+ package, but I don't want to block the conveniences documented here
+ for that.)
+
+with support
+------------
+
+We can now use the with statement to define transaction boundaries.
+
+.. doctest::
+
+    >>> import transaction.tests.savepointsample
+    >>> dm = transaction.tests.savepointsample.SampleSavepointDataManager()
+    >>> list(dm.keys())
+    []
+
+We can use it with a manager:
+
+.. doctest::
+
+    >>> with transaction.manager as t:
+    ...     dm['z'] = 3
+    ...     t.note('test 3')
+
+    >>> dm['z']
+    3
+
+    >>> dm.last_note
+    'test 3'
+
+    >>> with transaction.manager: #doctest ELLIPSIS
+    ...     dm['z'] = 4
+    ...     xxx
+    Traceback (most recent call last):
+    ...
+    NameError: ... name 'xxx' is not defined
+
+    >>> dm['z']
+    3
+
+On Python 2, you can also abbreviate ``with transaction.manager:`` as ``with
+transaction:``.  This does not work on Python 3 (see see
+http://bugs.python.org/issue12022).
+
+Retries
+-------
+
+Commits can fail for transient reasons, especially conflicts.
+Applications will often retry transactions some number of times to
+overcome transient failures.  This typically looks something like:
+
+.. doctest::
+
+    for i in range(3):
+        try:
+           with transaction.manager:
+               ... some something ...
+        except SomeTransientException:
+           contine
+        else:
+           break
+
+This is rather ugly.
+
+Transaction managers provide a helper for this case. To show this,
+we'll use a contrived example:
+
+.. doctest::
+
+    >>> ntry = 0
+    >>> with transaction.manager:
+    ...      dm['ntry'] = 0
+
+    >>> import transaction.interfaces
+    >>> class Retry(transaction.interfaces.TransientError):
+    ...     pass
+
+    >>> for attempt in transaction.manager.attempts():
+    ...     with attempt as t:
+    ...         t.note('test')
+    ...         print("%s %s" % (dm['ntry'], ntry))
+    ...         ntry += 1
+    ...         dm['ntry'] = ntry
+    ...         if ntry % 3:
+    ...             raise Retry(ntry)
+    0 0
+    0 1
+    0 2
+
+The raising of a subclass of TransientError is critical here. It's
+what signals that the transaction should be retried.  It is generally
+up to the data manager to signal that a transaction should try again
+by raising a subclass of TransientError (or TransientError itself, of
+course).
+
+You shouldn't make any assumptions about the object returned by the
+iterator.  (It isn't a transaction or transaction manager, as far as
+you know. :)  If you use the ``as`` keyword in the ``with`` statement,
+a transaction object will be assigned to the variable named.
+
+By default, it tries 3 times. We can tell it how many times to try:
+
+.. doctest::
+
+    >>> for attempt in transaction.manager.attempts(2):
+    ...     with attempt:
+    ...         ntry += 1
+    ...         if ntry % 3:
+    ...             raise Retry(ntry)
+    Traceback (most recent call last):
+    ...
+    Retry: 5
+
+It it doesn't succeed in that many times, the exception will be
+propagated.
+
+Of course, other errors are propagated directly:
+
+.. doctest::
+
+    >>> ntry = 0
+    >>> for attempt in transaction.manager.attempts():
+    ...     with attempt:
+    ...         ntry += 1
+    ...         if ntry == 3:
+    ...             raise ValueError(ntry)
+    Traceback (most recent call last):
+    ...
+    ValueError: 3
+
+We can use the default transaction manager:
+
+.. doctest::
+
+    >>> for attempt in transaction.attempts():
+    ...     with attempt as t:
+    ...         t.note('test')
+    ...         print("%s %s" % (dm['ntry'], ntry))
+    ...         ntry += 1
+    ...         dm['ntry'] = ntry
+    ...         if ntry % 3:
+    ...             raise Retry(ntry)
+    3 3
+    3 4
+    3 5
+
+Sometimes, a data manager doesn't raise exceptions directly, but
+wraps other other systems that raise exceptions outside of it's
+control.  Data  managers can provide a should_retry method that takes
+an exception instance and returns True if the transaction should be
+attempted again.
+
+.. doctest::
+
+    >>> class DM(transaction.tests.savepointsample.SampleSavepointDataManager):
+    ...     def should_retry(self, e):
+    ...         if 'should retry' in str(e):
+    ...             return True
+
+    >>> ntry = 0
+    >>> dm2 = DM()
+    >>> with transaction.manager:
+    ...     dm2['ntry'] = 0
+    >>> for attempt in transaction.manager.attempts():
+    ...     with attempt:
+    ...         print("%s %s" % (dm['ntry'], ntry))
+    ...         ntry += 1
+    ...         dm['ntry'] = ntry
+    ...         dm2['ntry'] = ntry
+    ...         if ntry % 3:
+    ...             raise ValueError('we really should retry this')
+    6 0
+    6 1
+    6 2
+
+    >>> dm2['ntry']
+    3

Copied: transaction/trunk/docs/datamanager.rst (from rev 128756, transaction/branches/sphinx/docs/datamanager.rst)
===================================================================
--- transaction/trunk/docs/datamanager.rst	                        (rev 0)
+++ transaction/trunk/docs/datamanager.rst	2012-12-18 05:26:55 UTC (rev 128757)
@@ -0,0 +1,391 @@
+Writing a Data Manager
+======================
+
+Simple Data Manager
+------------------
+
+.. doctest::
+
+   >>> from transaction.tests.examples import DataManager
+
+This :class:`transaction.tests.examples.DataManager` class
+provides a trivial data-manager implementation and docstrings to illustrate
+the the protocol and to provide a tool for writing tests.
+
+Our sample data manager has state that is updated through an inc
+method and through transaction operations.
+
+
+When we create a sample data manager:
+
+.. doctest::
+
+   >>> dm = DataManager()
+
+It has two bits of state, state:
+
+.. doctest::
+
+   >>> dm.state
+   0
+
+and delta:
+
+.. doctest::
+
+   >>> dm.delta
+   0
+
+Both of which are initialized to 0.  state is meant to model
+committed state, while delta represents tentative changes within a
+transaction.  We change the state by calling inc:
+
+.. doctest::
+
+   >>> dm.inc()
+
+which updates delta:
+
+.. doctest::
+
+   >>> dm.delta
+   1
+
+but state isn't changed until we commit the transaction:
+
+.. doctest::
+
+   >>> dm.state
+   0
+
+To commit the changes, we use 2-phase commit. We execute the first
+stage by calling prepare.  We need to pass a transation. Our
+sample data managers don't really use the transactions for much,
+so we'll be lazy and use strings for transactions:
+
+.. doctest::
+
+   >>> t1 = '1'
+   >>> dm.prepare(t1)
+
+The sample data manager updates the state when we call prepare:
+
+.. doctest::
+
+   >>> dm.state
+   1
+   >>> dm.delta
+   1
+
+This is mainly so we can detect some affect of calling the methods.
+
+Now if we call commit:
+
+.. doctest::
+
+   >>> dm.commit(t1)
+
+Our changes are"permanent".  The state reflects the changes and the
+delta has been reset to 0.
+
+.. doctest::
+
+   >>> dm.state
+   1
+   >>> dm.delta
+   0
+
+The :meth:`prepare` Method
+----------------------------
+
+Prepare to commit data
+
+.. doctest::
+
+   >>> dm = DataManager()
+   >>> dm.inc()
+   >>> t1 = '1'
+   >>> dm.prepare(t1)
+   >>> dm.commit(t1)
+   >>> dm.state
+   1
+   >>> dm.inc()
+   >>> t2 = '2'
+   >>> dm.prepare(t2)
+   >>> dm.abort(t2)
+   >>> dm.state
+   1
+
+It is en error to call prepare more than once without an intervening
+commit or abort:
+
+.. doctest::
+
+   >>> dm.prepare(t1)
+
+   >>> dm.prepare(t1)
+   Traceback (most recent call last):
+   ...
+   TypeError: Already prepared
+
+   >>> dm.prepare(t2)
+   Traceback (most recent call last):
+   ...
+   TypeError: Already prepared
+
+   >>> dm.abort(t1)
+
+If there was a preceeding savepoint, the transaction must match:
+
+.. doctest::
+
+   >>> rollback = dm.savepoint(t1)
+   >>> dm.prepare(t2)
+   Traceback (most recent call last):
+   ,,,
+   TypeError: ('Transaction missmatch', '2', '1')
+
+   >>> dm.prepare(t1)
+
+The :meth:`abort` method
+--------------------------
+
+The abort method can be called before two-phase commit to
+throw away work done in the transaction:
+
+.. doctest::
+
+   >>> dm = DataManager()
+   >>> dm.inc()
+   >>> dm.state, dm.delta
+   (0, 1)
+   >>> t1 = '1'
+   >>> dm.abort(t1)
+   >>> dm.state, dm.delta
+   (0, 0)
+
+The abort method also throws away work done in savepoints:
+
+.. doctest::
+
+   >>> dm.inc()
+   >>> r = dm.savepoint(t1)
+   >>> dm.inc()
+   >>> r = dm.savepoint(t1)
+   >>> dm.state, dm.delta
+   (0, 2)
+   >>> dm.abort(t1)
+   >>> dm.state, dm.delta
+   (0, 0)
+
+If savepoints are used, abort must be passed the same
+transaction:
+
+.. doctest::
+
+   >>> dm.inc()
+   >>> r = dm.savepoint(t1)
+   >>> t2 = '2'
+   >>> dm.abort(t2)
+   Traceback (most recent call last):
+   ...
+   TypeError: ('Transaction missmatch', '2', '1')
+
+   >>> dm.abort(t1)
+
+The abort method is also used to abort a two-phase commit:
+
+.. doctest::
+
+   >>> dm.inc()
+   >>> dm.state, dm.delta
+   (0, 1)
+   >>> dm.prepare(t1)
+   >>> dm.state, dm.delta
+   (1, 1)
+   >>> dm.abort(t1)
+   >>> dm.state, dm.delta
+   (0, 0)
+
+Of course, the transactions passed to prepare and abort must
+match:
+
+.. doctest::
+
+   >>> dm.prepare(t1)
+   >>> dm.abort(t2)
+   Traceback (most recent call last):
+   ...
+   TypeError: ('Transaction missmatch', '2', '1')
+
+   >>> dm.abort(t1)
+
+
+
+The :meth:`commit` method
+---------------------------
+
+Called to omplete two-phase commit
+
+.. doctest::
+
+   >>> dm = DataManager()
+   >>> dm.state
+   0
+   >>> dm.inc()
+
+We start two-phase commit by calling prepare:
+
+.. doctest::
+
+   >>> t1 = '1'
+   >>> dm.prepare(t1)
+
+   We complete it by calling commit:
+
+.. doctest::
+
+   >>> dm.commit(t1)
+   >>> dm.state
+   1
+
+It is an error ro call commit without calling prepare first:
+
+.. doctest::
+
+   >>> dm.inc()
+   >>> t2 = '2'
+   >>> dm.commit(t2)
+   Traceback (most recent call last):
+   ...
+   TypeError: Not prepared to commit
+
+   >>> dm.prepare(t2)
+   >>> dm.commit(t2)
+
+If course, the transactions given to prepare and commit must
+be the same:
+
+.. doctest::
+
+   >>> dm.inc()
+   >>> t3 = '3'
+   >>> dm.prepare(t3)
+   >>> dm.commit(t2)
+   Traceback (most recent call last):
+   ...
+   TypeError: ('Transaction missmatch', '2', '3')
+
+
+The :meth:`savepoint` method
+------------------------------
+
+Provide the ability to rollback transaction state
+
+Savepoints provide a way to:
+
+ - Save partial transaction work. For some data managers, this
+   could allow resources to be used more efficiently.
+
+ - Provide the ability to revert state to a point in a
+   transaction without aborting the entire transaction.  In
+   other words, savepoints support partial aborts.
+
+Savepoints don't use two-phase commit. If there are errors in
+setting or rolling back to savepoints, the application should
+abort the containing transaction.  This is *not* the
+responsibility of the data manager.
+
+Savepoints are always associated with a transaction. Any work
+done in a savepoint's transaction is tentative until the
+transaction is committed using two-phase commit.
+
+.. doctest::
+
+   >>> dm = DataManager()
+   >>> dm.inc()
+   >>> t1 = '1'
+   >>> r = dm.savepoint(t1)
+   >>> dm.state, dm.delta
+   (0, 1)
+   >>> dm.inc()
+   >>> dm.state, dm.delta
+   (0, 2)
+   >>> r.rollback()
+   >>> dm.state, dm.delta
+   (0, 1)
+   >>> dm.prepare(t1)
+   >>> dm.commit(t1)
+   >>> dm.state, dm.delta
+   (1, 0)
+
+Savepoints must have the same transaction:
+
+.. doctest::
+
+   >>> r1 = dm.savepoint(t1)
+   >>> dm.state, dm.delta
+   (1, 0)
+   >>> dm.inc()
+   >>> dm.state, dm.delta
+   (1, 1)
+   >>> t2 = '2'
+   >>> r2 = dm.savepoint(t2)
+   Traceback (most recent call last):
+   ...
+   TypeError: ('Transaction missmatch', '2', '1')
+
+   >>> r2 = dm.savepoint(t1)
+   >>> dm.inc()
+   >>> dm.state, dm.delta
+   (1, 2)
+
+If we rollback to an earlier savepoint, we discard all work
+done later:
+
+.. doctest::
+
+   >>> r1.rollback()
+   >>> dm.state, dm.delta
+   (1, 0)
+
+and we can no longer rollback to the later savepoint:
+
+.. doctest::
+
+   >>> r2.rollback()
+   Traceback (most recent call last):
+   ...
+   TypeError: ('Attempt to roll back to invalid save point', 3, 2)
+
+We can roll back to a savepoint as often as we like:
+
+.. doctest::
+
+   >>> r1.rollback()
+   >>> r1.rollback()
+   >>> r1.rollback()
+   >>> dm.state, dm.delta
+   (1, 0)
+
+   >>> dm.inc()
+   >>> dm.inc()
+   >>> dm.inc()
+   >>> dm.state, dm.delta
+   (1, 3)
+   >>> r1.rollback()
+   >>> dm.state, dm.delta
+   (1, 0)
+
+But we can't rollback to a savepoint after it has been
+committed:
+
+.. doctest::
+
+   >>> dm.prepare(t1)
+   >>> dm.commit(t1)
+
+   >>> r1.rollback()
+   Traceback (most recent call last):
+   ...
+   TypeError: Attempt to rollback stale rollback

Copied: transaction/trunk/docs/doom.rst (from rev 128495, transaction/trunk/transaction/tests/doom.txt)
===================================================================
--- transaction/trunk/docs/doom.rst	                        (rev 0)
+++ transaction/trunk/docs/doom.rst	2012-12-18 05:26:55 UTC (rev 128757)
@@ -0,0 +1,164 @@
+Dooming Transactions
+====================
+
+A doomed transaction behaves exactly the same way as an active transaction but
+raises an error on any attempt to commit it, thus forcing an abort.
+
+Doom is useful in places where abort is unsafe and an exception cannot be
+raised.  This occurs when the programmer wants the code following the doom to
+run but not commit. It is unsafe to abort in these circumstances as a following
+get() may implicitly open a new transaction.
+
+Any attempt to commit a doomed transaction will raise a DoomedTransaction
+exception.
+
+An example of such a use case can be found in
+zope/app/form/browser/editview.py.  Here a form validation failure must doom
+the transaction as committing the transaction may have side-effects. However,
+the form code must continue to calculate a form containing the error messages
+to return.
+
+For Zope in general, code running within a request should always doom
+transactions rather than aborting them. It is the responsibilty of the
+publication to either abort() or commit() the transaction. Application code can
+use savepoints and doom() safely.
+
+To see how it works we first need to create a stub data manager:
+
+.. doctest::
+
+    >>> from transaction.interfaces import IDataManager
+    >>> from zope.interface import implementer
+    >>> @implementer(IDataManager)
+    ... class DataManager:
+    ...     def __init__(self):
+    ...         self.attr_counter = {}
+    ...     def __getattr__(self, name):
+    ...         def f(transaction):
+    ...             self.attr_counter[name] = self.attr_counter.get(name, 0) + 1
+    ...         return f
+    ...     def total(self):
+    ...         count = 0
+    ...         for access_count in self.attr_counter.values():
+    ...             count += access_count
+    ...         return count
+    ...     def sortKey(self):
+    ...         return 1
+
+Start a new transaction:
+
+.. doctest::
+
+    >>> import transaction
+    >>> txn = transaction.begin()
+    >>> dm = DataManager()
+    >>> txn.join(dm)
+
+We can ask a transaction if it is doomed to avoid expensive operations. An
+example of a use case is an object-relational mapper where a pre-commit hook
+sends all outstanding SQL to a relational database for objects changed during
+the transaction. This expensive operation is not necessary if the transaction
+has been doomed. A non-doomed transaction should return False:
+
+.. doctest::
+
+    >>> txn.isDoomed()
+    False
+
+We can doom a transaction by calling .doom() on it:
+
+.. doctest::
+
+    >>> txn.doom()
+    >>> txn.isDoomed()
+    True
+
+We can doom it again if we like:
+
+.. doctest::
+
+    >>> txn.doom()
+
+The data manager is unchanged at this point:
+
+.. doctest::
+
+    >>> dm.total()
+    0
+
+Attempting to commit a doomed transaction any number of times raises a
+DoomedTransaction:
+
+.. doctest::
+
+    >>> txn.commit() # doctest: +IGNORE_EXCEPTION_DETAIL
+    Traceback (most recent call last):
+    DoomedTransaction: transaction doomed, cannot commit
+    >>> txn.commit() # doctest: +IGNORE_EXCEPTION_DETAIL
+    Traceback (most recent call last):
+    DoomedTransaction: transaction doomed, cannot commit
+
+But still leaves the data manager unchanged:
+
+.. doctest::
+
+    >>> dm.total()
+    0
+
+But the doomed transaction can be aborted:
+
+.. doctest::
+
+    >>> txn.abort()
+
+Which aborts the data manager:
+
+.. doctest::
+
+    >>> dm.total()
+    1
+    >>> dm.attr_counter['abort']
+    1
+
+Dooming the current transaction can also be done directly from the transaction
+module. We can also begin a new transaction directly after dooming the old one:
+
+.. doctest::
+
+    >>> txn = transaction.begin()
+    >>> transaction.isDoomed()
+    False
+    >>> transaction.doom()
+    >>> transaction.isDoomed()
+    True
+    >>> txn = transaction.begin()
+
+After committing a transaction we get an assertion error if we try to doom the
+transaction. This could be made more specific, but trying to doom a transaction
+after it's been committed is probably a programming error:
+
+.. doctest::
+
+    >>> txn = transaction.begin()
+    >>> txn.commit()
+    >>> txn.doom()
+    Traceback (most recent call last):
+        ...
+    ValueError: non-doomable
+
+A doomed transaction should act the same as an active transaction, so we should
+be able to join it:
+
+.. doctest::
+
+    >>> txn = transaction.begin()
+    >>> txn.doom()
+    >>> dm2 = DataManager()
+    >>> txn.join(dm2)
+
+Clean up:
+
+.. doctest::
+
+    >>> txn = transaction.begin()
+    >>> txn.abort()

Copied: transaction/trunk/docs/hooks.rst (from rev 128756, transaction/branches/sphinx/docs/hooks.rst)
===================================================================
--- transaction/trunk/docs/hooks.rst	                        (rev 0)
+++ transaction/trunk/docs/hooks.rst	2012-12-18 05:26:55 UTC (rev 128757)
@@ -0,0 +1,384 @@
+Hooking the Transaction Machinery
+=================================
+
+The :meth:`addBeforeCommitHook` Method
+--------------------------------------
+
+Let's define a hook to call, and a way to see that it was called.
+
+.. doctest::
+
+    >>> log = []
+    >>> def reset_log():
+    ...     del log[:]
+
+    >>> def hook(arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
+    ...     log.append("arg %r kw1 %r kw2 %r" % (arg, kw1, kw2))
+
+Now register the hook with a transaction.
+
+.. doctest::
+
+    >>> from transaction import begin
+    >>> from transaction._compat import func_name
+    >>> import transaction
+    >>> t = begin()
+    >>> t.addBeforeCommitHook(hook, '1')
+
+We can see that the hook is indeed registered.
+
+.. doctest::
+
+    >>> [(func_name(hook), args, kws)
+    ...  for hook, args, kws in t.getBeforeCommitHooks()]
+    [('hook', ('1',), {})]
+
+When transaction commit starts, the hook is called, with its
+arguments.
+
+.. doctest::
+
+    >>> log
+    []
+    >>> t.commit()
+    >>> log
+    ["arg '1' kw1 'no_kw1' kw2 'no_kw2'"]
+    >>> reset_log()
+
+A hook's registration is consumed whenever the hook is called.  Since
+the hook above was called, it's no longer registered:
+
+.. doctest::
+
+    >>> from transaction import commit
+    >>> len(list(t.getBeforeCommitHooks()))
+    0
+    >>> commit()
+    >>> log
+    []
+
+The hook is only called for a full commit, not for a savepoint.
+
+.. doctest::
+
+    >>> t = begin()
+    >>> t.addBeforeCommitHook(hook, 'A', dict(kw1='B'))
+    >>> dummy = t.savepoint()
+    >>> log
+    []
+    >>> t.commit()
+    >>> log
+    ["arg 'A' kw1 'B' kw2 'no_kw2'"]
+    >>> reset_log()
+
+If a transaction is aborted, no hook is called.
+
+.. doctest::
+
+    >>> from transaction import abort
+    >>> t = begin()
+    >>> t.addBeforeCommitHook(hook, ["OOPS!"])
+    >>> abort()
+    >>> log
+    []
+    >>> commit()
+    >>> log
+    []
+
+The hook is called before the commit does anything, so even if the
+commit fails the hook will have been called.  To provoke failures in
+commit, we'll add failing resource manager to the transaction.
+
+.. doctest::
+
+    >>> class CommitFailure(Exception):
+    ...     pass
+    >>> class FailingDataManager:
+    ...     def tpc_begin(self, txn, sub=False):
+    ...         raise CommitFailure('failed')
+    ...     def abort(self, txn):
+    ...         pass
+
+    >>> t = begin()
+    >>> t.join(FailingDataManager())
+
+    >>> t.addBeforeCommitHook(hook, '2')
+
+    >>> from transaction.tests.common import DummyFile
+    >>> from transaction.tests.common import Monkey
+    >>> from transaction.tests.common import assertRaisesEx
+    >>> from transaction import _transaction
+    >>> buffer = DummyFile()
+    >>> with Monkey(_transaction, _TB_BUFFER=buffer):
+    ...     err = assertRaisesEx(CommitFailure, t.commit)
+    >>> log
+    ["arg '2' kw1 'no_kw1' kw2 'no_kw2'"]
+    >>> reset_log()
+
+Let's register several hooks.
+
+.. doctest::
+
+    >>> t = begin()
+    >>> t.addBeforeCommitHook(hook, '4', dict(kw1='4.1'))
+    >>> t.addBeforeCommitHook(hook, '5', dict(kw2='5.2'))
+
+They are returned in the same order by getBeforeCommitHooks.
+
+.. doctest::
+
+    >>> [(func_name(hook), args, kws)  #doctest: +NORMALIZE_WHITESPACE
+    ...  for hook, args, kws in t.getBeforeCommitHooks()]
+    [('hook', ('4',), {'kw1': '4.1'}),
+    ('hook', ('5',), {'kw2': '5.2'})]
+
+And commit also calls them in this order.
+
+.. doctest::
+
+    >>> t.commit()
+    >>> len(log)
+    2
+    >>> log  #doctest: +NORMALIZE_WHITESPACE
+    ["arg '4' kw1 '4.1' kw2 'no_kw2'",
+    "arg '5' kw1 'no_kw1' kw2 '5.2'"]
+    >>> reset_log()
+
+While executing, a hook can itself add more hooks, and they will all
+be called before the real commit starts.
+
+.. doctest::
+
+    >>> def recurse(txn, arg):
+    ...     log.append('rec' + str(arg))
+    ...     if arg:
+    ...         txn.addBeforeCommitHook(hook, '-')
+    ...         txn.addBeforeCommitHook(recurse, (txn, arg-1))
+
+    >>> t = begin()
+    >>> t.addBeforeCommitHook(recurse, (t, 3))
+    >>> commit()
+    >>> log  #doctest: +NORMALIZE_WHITESPACE
+    ['rec3',
+            "arg '-' kw1 'no_kw1' kw2 'no_kw2'",
+    'rec2',
+            "arg '-' kw1 'no_kw1' kw2 'no_kw2'",
+    'rec1',
+            "arg '-' kw1 'no_kw1' kw2 'no_kw2'",
+    'rec0']
+    >>> reset_log()
+
+The :meth:`addAfterCommitHook` Method
+--------------------------------------
+
+Let's define a hook to call, and a way to see that it was called.
+
+.. doctest::
+
+    >>> log = []
+    >>> def reset_log():
+    ...     del log[:]
+
+    >>> def hook(status, arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
+    ...     log.append("%r arg %r kw1 %r kw2 %r" % (status, arg, kw1, kw2))
+
+Now register the hook with a transaction.
+
+.. doctest::
+
+    >>> from transaction import begin
+    >>> from transaction._compat import func_name
+    >>> t = begin()
+    >>> t.addAfterCommitHook(hook, '1')
+
+We can see that the hook is indeed registered.
+
+.. doctest::
+
+
+    >>> [(func_name(hook), args, kws)
+    ...  for hook, args, kws in t.getAfterCommitHooks()]
+    [('hook', ('1',), {})]
+
+When transaction commit is done, the hook is called, with its
+arguments.
+
+.. doctest::
+
+    >>> log
+    []
+    >>> t.commit()
+    >>> log
+    ["True arg '1' kw1 'no_kw1' kw2 'no_kw2'"]
+    >>> reset_log()
+
+A hook's registration is consumed whenever the hook is called.  Since
+the hook above was called, it's no longer registered:
+
+.. doctest::
+
+    >>> from transaction import commit
+    >>> len(list(t.getAfterCommitHooks()))
+    0
+    >>> commit()
+    >>> log
+    []
+
+The hook is only called after a full commit, not for a savepoint.
+
+.. doctest::
+
+    >>> t = begin()
+    >>> t.addAfterCommitHook(hook, 'A', dict(kw1='B'))
+    >>> dummy = t.savepoint()
+    >>> log
+    []
+    >>> t.commit()
+    >>> log
+    ["True arg 'A' kw1 'B' kw2 'no_kw2'"]
+    >>> reset_log()
+
+If a transaction is aborted, no hook is called.
+
+.. doctest::
+
+    >>> from transaction import abort
+    >>> t = begin()
+    >>> t.addAfterCommitHook(hook, ["OOPS!"])
+    >>> abort()
+    >>> log
+    []
+    >>> commit()
+    >>> log
+    []
+
+The hook is called after the commit is done, so even if the
+commit fails the hook will have been called.  To provoke failures in
+commit, we'll add failing resource manager to the transaction.
+
+.. doctest::
+
+    >>> class CommitFailure(Exception):
+    ...     pass
+    >>> class FailingDataManager:
+    ...     def tpc_begin(self, txn):
+    ...         raise CommitFailure('failed')
+    ...     def abort(self, txn):
+    ...         pass
+
+    >>> t = begin()
+    >>> t.join(FailingDataManager())
+
+    >>> t.addAfterCommitHook(hook, '2')
+    >>> from transaction.tests.common import DummyFile
+    >>> from transaction.tests.common import Monkey
+    >>> from transaction.tests.common import assertRaisesEx
+    >>> from transaction import _transaction
+    >>> buffer = DummyFile()
+    >>> with Monkey(_transaction, _TB_BUFFER=buffer):
+    ...     err = assertRaisesEx(CommitFailure, t.commit)
+    >>> log
+    ["False arg '2' kw1 'no_kw1' kw2 'no_kw2'"]
+    >>> reset_log()
+
+Let's register several hooks.
+
+.. doctest::
+
+    >>> t = begin()
+    >>> t.addAfterCommitHook(hook, '4', dict(kw1='4.1'))
+    >>> t.addAfterCommitHook(hook, '5', dict(kw2='5.2'))
+
+They are returned in the same order by getAfterCommitHooks.
+
+.. doctest::
+
+    >>> [(func_name(hook), args, kws)     #doctest: +NORMALIZE_WHITESPACE
+    ...  for hook, args, kws in t.getAfterCommitHooks()]
+    [('hook', ('4',), {'kw1': '4.1'}),
+    ('hook', ('5',), {'kw2': '5.2'})]
+
+And commit also calls them in this order.
+
+.. doctest::
+
+    >>> t.commit()
+    >>> len(log)
+    2
+    >>> log  #doctest: +NORMALIZE_WHITESPACE
+    ["True arg '4' kw1 '4.1' kw2 'no_kw2'",
+    "True arg '5' kw1 'no_kw1' kw2 '5.2'"]
+    >>> reset_log()
+
+While executing, a hook can itself add more hooks, and they will all
+be called before the real commit starts.
+
+.. doctest::
+
+    >>> def recurse(status, txn, arg):
+    ...     log.append('rec' + str(arg))
+    ...     if arg:
+    ...         txn.addAfterCommitHook(hook, '-')
+    ...         txn.addAfterCommitHook(recurse, (txn, arg-1))
+
+    >>> t = begin()
+    >>> t.addAfterCommitHook(recurse, (t, 3))
+    >>> commit()
+    >>> log  #doctest: +NORMALIZE_WHITESPACE
+    ['rec3',
+            "True arg '-' kw1 'no_kw1' kw2 'no_kw2'",
+    'rec2',
+            "True arg '-' kw1 'no_kw1' kw2 'no_kw2'",
+    'rec1',
+            "True arg '-' kw1 'no_kw1' kw2 'no_kw2'",
+    'rec0']
+    >>> reset_log()
+
+If an after commit hook is raising an exception then it will log a
+message at error level so that if other hooks are registered they
+can be executed. We don't support execution dependencies at this level.
+
+.. doctest::
+
+    >>> from transaction import TransactionManager
+    >>> from transaction.tests.test__manager import DataObject
+    >>> mgr = TransactionManager()
+    >>> do = DataObject(mgr)
+
+    >>> def hookRaise(status, arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
+    ...     raise TypeError("Fake raise")
+
+    >>> t = begin()
+
+    >>> t.addAfterCommitHook(hook, ('-', 1))
+    >>> t.addAfterCommitHook(hookRaise, ('-', 2))
+    >>> t.addAfterCommitHook(hook, ('-', 3))
+    >>> commit()
+
+    >>> log
+    ["True arg '-' kw1 1 kw2 'no_kw2'", "True arg '-' kw1 3 kw2 'no_kw2'"]
+
+    >>> reset_log()
+
+Test that the associated transaction manager has been cleanup when
+after commit hooks are registered
+
+.. doctest::
+
+    >>> mgr = TransactionManager()
+    >>> do = DataObject(mgr)
+
+    >>> t = begin()
+    >>> t._manager._txn is not None
+    True
+
+    >>> t.addAfterCommitHook(hook, ('-', 1))
+    >>> commit()
+
+    >>> log
+    ["True arg '-' kw1 1 kw2 'no_kw2'"]
+
+    >>> t._manager._txn is not None
+    False
+
+    >>> reset_log()

Modified: transaction/trunk/docs/index.rst
===================================================================
--- transaction/trunk/docs/index.rst	2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/docs/index.rst	2012-12-18 05:26:55 UTC (rev 128757)
@@ -1,11 +1,102 @@
 :mod:`transaction` Documentation
 ================================
 
+Transaction objects manage resources for an individual activity.
+
+Compatibility issues
+--------------------
+
+The implementation of Transaction objects involves two layers of
+backwards compatibility, because this version of transaction supports
+both ZODB 3 and ZODB 4.  Zope is evolving towards the ZODB4
+interfaces.
+
+Transaction has two methods for a resource manager to call to
+participate in a transaction -- register() and join().  join() takes a
+resource manager and adds it to the list of resources.  register() is
+for backwards compatibility.  It takes a persistent object and
+registers its _p_jar attribute.  TODO: explain adapter
+
+Two-phase commit
+----------------
+
+A transaction commit involves an interaction between the transaction
+object and one or more resource managers.  The transaction manager
+calls the following four methods on each resource manager; it calls
+tpc_begin() on each resource manager before calling commit() on any of
+them.
+
+    1. tpc_begin(txn)
+    2. commit(txn)
+    3. tpc_vote(txn)
+    4. tpc_finish(txn)
+
+Before-commit hook
+------------------
+
+Sometimes, applications want to execute some code when a transaction is
+committed.  For example, one might want to delay object indexing until a
+transaction commits, rather than indexing every time an object is changed.
+Or someone might want to check invariants only after a set of operations.  A
+pre-commit hook is available for such use cases:  use addBeforeCommitHook(),
+passing it a callable and arguments.  The callable will be called with its
+arguments at the start of the commit (but not for substransaction commits).
+
+After-commit hook
+------------------
+
+Sometimes, applications want to execute code after a transaction commit
+attempt succeeds or aborts. For example, one might want to launch non
+transactional code after a successful commit. Or still someone might
+want to launch asynchronous code after.  A post-commit hook is
+available for such use cases: use addAfterCommitHook(), passing it a
+callable and arguments.  The callable will be called with a Boolean
+value representing the status of the commit operation as first
+argument (true if successfull or false iff aborted) preceding its
+arguments at the start of the commit (but not for substransaction
+commits). Commit hooks are not called for transaction.abort().
+
+Error handling
+--------------
+
+When errors occur during two-phase commit, the transaction manager
+aborts all the resource managers.  The specific methods it calls
+depend on whether the error occurs before or after the call to
+tpc_vote() on that transaction manager.
+
+If the resource manager has not voted, then the resource manager will
+have one or more uncommitted objects.  There are two cases that lead
+to this state; either the transaction manager has not called commit()
+for any objects on this resource manager or the call that failed was a
+commit() for one of the objects of this resource manager.  For each
+uncommitted object, including the object that failed in its commit(),
+call abort().
+
+Once uncommitted objects are aborted, tpc_abort() or abort_sub() is
+called on each resource manager.
+
+Synchronization
+---------------
+
+You can register sychronization objects (synchronizers) with the
+tranasction manager.  The synchronizer must implement
+beforeCompletion() and afterCompletion() methods.  The transaction
+manager calls beforeCompletion() when it starts a top-level two-phase
+commit.  It calls afterCompletion() when a top-level transaction is
+committed or aborted.  The methods are passed the current Transaction
+as their only argument.
+
 Contents:
 
 .. toctree::
    :maxdepth: 2
 
+   convenience
+   doom
+   savepoint
+   hooks
+   datamanager
+   resourcemanager
    api
 
 

Copied: transaction/trunk/docs/resourcemanager.rst (from rev 128756, transaction/branches/sphinx/docs/resourcemanager.rst)
===================================================================
--- transaction/trunk/docs/resourcemanager.rst	                        (rev 0)
+++ transaction/trunk/docs/resourcemanager.rst	2012-12-18 05:26:55 UTC (rev 128757)
@@ -0,0 +1,399 @@
+Writing a Resource Manager
+==========================
+
+Simple Resource Manager
+-----------------------
+
+.. doctest::
+
+   >>> from transaction.tests.examples import ResourceManager
+
+This :class:`transaction.tests.examples.ResourceManager`
+class provides a trivial resource-manager implementation and doc
+strings to illustrate the protocol and to provide a tool for writing
+tests.
+
+Our sample resource manager has state that is updated through an inc
+method and through transaction operations.
+
+When we create a sample resource manager:
+
+.. doctest::
+
+   >>> rm = ResourceManager()
+
+It has two pieces state, state and delta, both initialized to 0:
+
+.. doctest::
+
+   >>> rm.state
+   0
+   >>> rm.delta
+   0
+
+state is meant to model committed state, while delta represents
+tentative changes within a transaction.  We change the state by
+calling inc:
+
+.. doctest::
+
+   >>> rm.inc()
+
+which updates delta:
+
+.. doctest::
+
+   >>> rm.delta
+   1
+
+but state isn't changed until we commit the transaction:
+
+.. doctest::
+
+   >>> rm.state
+   0
+
+To commit the changes, we use 2-phase commit.  We execute the first
+stage by calling prepare.  We need to pass a transation. Our
+sample resource managers don't really use the transactions for much,
+so we'll be lazy and use strings for transactions.  The sample
+resource manager updates the state when we call tpc_vote:
+
+
+.. doctest::
+
+   >>> t1 = '1'
+   >>> rm.tpc_begin(t1)
+   >>> rm.state, rm.delta
+   (0, 1)
+
+   >>> rm.tpc_vote(t1)
+   >>> rm.state, rm.delta
+   (1, 1)
+
+   Now if we call tpc_finish:
+
+   >>> rm.tpc_finish(t1)
+
+Our changes are "permanent".  The state reflects the changes and the
+delta has been reset to 0.
+
+.. doctest::
+
+   >>> rm.state, rm.delta
+   (1, 0)
+
+
+The :meth:`tpc_begin` Method
+-----------------------------
+
+Called by the transaction manager to ask the RM to prepare to commit data.
+
+.. doctest::
+
+   >>> rm = ResourceManager()
+   >>> rm.inc()
+   >>> t1 = '1'
+   >>> rm.tpc_begin(t1)
+   >>> rm.tpc_vote(t1)
+   >>> rm.tpc_finish(t1)
+   >>> rm.state
+   1
+   >>> rm.inc()
+   >>> t2 = '2'
+   >>> rm.tpc_begin(t2)
+   >>> rm.tpc_vote(t2)
+   >>> rm.tpc_abort(t2)
+   >>> rm.state
+   1
+
+It is an error to call tpc_begin more than once without completing
+two-phase commit:
+
+.. doctest::
+
+   >>> rm.tpc_begin(t1)
+
+   >>> rm.tpc_begin(t1)
+   Traceback (most recent call last):
+   ...
+   ValueError: txn in state 'tpc_begin' but expected one of (None,)
+   >>> rm.tpc_abort(t1)
+
+If there was a preceeding savepoint, the transaction must match:
+
+.. doctest::
+
+   >>> rollback = rm.savepoint(t1)
+   >>> rm.tpc_begin(t2)
+   Traceback (most recent call last):
+   ,,,
+   TypeError: ('Transaction missmatch', '2', '1')
+
+   >>> rm.tpc_begin(t1)
+
+
+The :meth:`tpc_vote` Method
+---------------------------
+
+Verify that a data manager can commit the transaction.
+
+This is the last chance for a data manager to vote 'no'.  A
+data manager votes 'no' by raising an exception.
+
+Passed `transaction`, which is the ITransaction instance associated with the
+transaction being committed.
+
+
+The :meth:`tpc_finish` Method
+-----------------------------
+
+Complete two-phase commit
+
+.. doctest::
+
+   >>> rm = ResourceManager()
+   >>> rm.state
+   0
+   >>> rm.inc()
+
+   We start two-phase commit by calling prepare:
+
+   >>> t1 = '1'
+   >>> rm.tpc_begin(t1)
+   >>> rm.tpc_vote(t1)
+
+   We complete it by calling tpc_finish:
+
+   >>> rm.tpc_finish(t1)
+   >>> rm.state
+   1
+
+It is an error ro call tpc_finish without calling tpc_vote:
+
+.. doctest::
+
+   >>> rm.inc()
+   >>> t2 = '2'
+   >>> rm.tpc_begin(t2)
+   >>> rm.tpc_finish(t2)
+   Traceback (most recent call last):
+   ...
+   ValueError: txn in state 'tpc_begin' but expected one of ('tpc_vote',)
+
+   >>> rm.tpc_abort(t2)  # clean slate
+
+   >>> rm.tpc_begin(t2)
+   >>> rm.tpc_vote(t2)
+   >>> rm.tpc_finish(t2)
+
+Of course, the transactions given to tpc_begin and tpc_finish must
+be the same:
+
+.. doctest::
+
+   >>> rm.inc()
+   >>> t3 = '3'
+   >>> rm.tpc_begin(t3)
+   >>> rm.tpc_vote(t3)
+   >>> rm.tpc_finish(t2)
+   Traceback (most recent call last):
+   ...
+   TypeError: ('Transaction missmatch', '2', '3')
+
+
+The :meth:`tpc_abort` Method
+-----------------------------
+
+Abort a transaction
+
+The abort method can be called before two-phase commit to
+throw away work done in the transaction:
+
+.. doctest::
+
+   >>> rm = ResourceManager()
+   >>> rm.inc()
+   >>> rm.state, rm.delta
+   (0, 1)
+   >>> t1 = '1'
+   >>> rm.tpc_abort(t1)
+   >>> rm.state, rm.delta
+   (0, 0)
+
+The abort method also throws away work done in savepoints:
+
+.. doctest::
+
+   >>> rm.inc()
+   >>> r = rm.savepoint(t1)
+   >>> rm.inc()
+   >>> r = rm.savepoint(t1)
+   >>> rm.state, rm.delta
+   (0, 2)
+   >>> rm.tpc_abort(t1)
+   >>> rm.state, rm.delta
+   (0, 0)
+
+If savepoints are used, abort must be passed the same
+transaction:
+
+.. doctest::
+
+   >>> rm.inc()
+   >>> r = rm.savepoint(t1)
+   >>> t2 = '2'
+   >>> rm.tpc_abort(t2)
+   Traceback (most recent call last):
+   ...
+   TypeError: ('Transaction missmatch', '2', '1')
+
+   >>> rm.tpc_abort(t1)
+
+The abort method is also used to abort a two-phase commit:
+
+.. doctest::
+
+   >>> rm.inc()
+   >>> rm.state, rm.delta
+   (0, 1)
+   >>> rm.tpc_begin(t1)
+   >>> rm.state, rm.delta
+   (0, 1)
+   >>> rm.tpc_vote(t1)
+   >>> rm.state, rm.delta
+   (1, 1)
+   >>> rm.tpc_abort(t1)
+   >>> rm.state, rm.delta
+   (0, 0)
+
+Of course, the transactions passed to prepare and abort must
+match:
+
+.. doctest::
+
+   >>> rm.tpc_begin(t1)
+   >>> rm.tpc_abort(t2)
+   Traceback (most recent call last):
+   ...
+   TypeError: ('Transaction missmatch', '2', '1')
+
+   >>> rm.tpc_abort(t1)
+
+This should never fail.
+
+
+The :meth:`savepoint` Method
+----------------------------
+
+Provide the ability to rollback transaction state
+
+Savepoints provide a way to:
+
+ - Save partial transaction work. For some resource managers, this
+   could allow resources to be used more efficiently.
+
+ - Provide the ability to revert state to a point in a
+   transaction without aborting the entire transaction.  In
+   other words, savepoints support partial aborts.
+
+Savepoints don't use two-phase commit. If there are errors in
+setting or rolling back to savepoints, the application should
+abort the containing transaction.  This is *not* the
+responsibility of the resource manager.
+
+Savepoints are always associated with a transaction. Any work
+done in a savepoint's transaction is tentative until the
+transaction is committed using two-phase commit.
+
+.. doctest::
+
+   >>> rm = ResourceManager()
+   >>> rm.inc()
+   >>> t1 = '1'
+   >>> r = rm.savepoint(t1)
+   >>> rm.state, rm.delta
+   (0, 1)
+   >>> rm.inc()
+   >>> rm.state, rm.delta
+   (0, 2)
+   >>> r.rollback()
+   >>> rm.state, rm.delta
+   (0, 1)
+   >>> rm.tpc_begin(t1)
+   >>> rm.tpc_vote(t1)
+   >>> rm.tpc_finish(t1)
+   >>> rm.state, rm.delta
+   (1, 0)
+
+Savepoints must have the same transaction:
+
+.. doctest::
+
+   >>> r1 = rm.savepoint(t1)
+   >>> rm.state, rm.delta
+   (1, 0)
+   >>> rm.inc()
+   >>> rm.state, rm.delta
+   (1, 1)
+   >>> t2 = '2'
+   >>> r2 = rm.savepoint(t2)
+   Traceback (most recent call last):
+   ...
+   TypeError: ('Transaction missmatch', '2', '1')
+
+   >>> r2 = rm.savepoint(t1)
+   >>> rm.inc()
+   >>> rm.state, rm.delta
+   (1, 2)
+
+If we rollback to an earlier savepoint, we discard all work
+done later:
+
+.. doctest::
+
+   >>> r1.rollback()
+   >>> rm.state, rm.delta
+   (1, 0)
+
+and we can no longer rollback to the later savepoint:
+
+.. doctest::
+
+   >>> r2.rollback()
+   Traceback (most recent call last):
+   ...
+   TypeError: ('Attempt to roll back to invalid save point', 3, 2)
+
+We can roll back to a savepoint as often as we like:
+
+.. doctest::
+
+   >>> r1.rollback()
+   >>> r1.rollback()
+   >>> r1.rollback()
+   >>> rm.state, rm.delta
+   (1, 0)
+
+   >>> rm.inc()
+   >>> rm.inc()
+   >>> rm.inc()
+   >>> rm.state, rm.delta
+   (1, 3)
+   >>> r1.rollback()
+   >>> rm.state, rm.delta
+   (1, 0)
+
+But we can't rollback to a savepoint after it has been
+committed:
+
+.. doctest::
+
+   >>> rm.tpc_begin(t1)
+   >>> rm.tpc_vote(t1)
+   >>> rm.tpc_finish(t1)
+
+   >>> r1.rollback()
+   Traceback (most recent call last):
+   ...
+   TypeError: Attempt to rollback stale rollback

Copied: transaction/trunk/docs/savepoint.rst (from rev 128495, transaction/trunk/transaction/tests/savepoint.txt)
===================================================================
--- transaction/trunk/docs/savepoint.rst	                        (rev 0)
+++ transaction/trunk/docs/savepoint.rst	2012-12-18 05:26:55 UTC (rev 128757)
@@ -0,0 +1,326 @@
+Savepoints
+==========
+
+Savepoints provide a way to save to disk intermediate work done during
+a transaction allowing:
+
+- partial transaction (subtransaction) rollback (abort)
+
+- state of saved objects to be freed, freeing on-line memory for other
+  uses
+
+Savepoints make it possible to write atomic subroutines that don't
+make top-level transaction commitments.
+
+
+Applications
+------------
+
+To demonstrate how savepoints work with transactions, we've provided a sample
+data manager implementation that provides savepoint support.  The primary
+purpose of this data manager is to provide code that can be read to understand
+how savepoints work.  The secondary purpose is to provide support for
+demonstrating the correct operation of savepoint support within the
+transaction system.  This data manager is very simple.  It provides flat
+storage of named immutable values, like strings and numbers.
+
+.. doctest::
+
+    >>> import transaction
+    >>> from transaction.tests import savepointsample
+    >>> dm = savepointsample.SampleSavepointDataManager()
+    >>> dm['name'] = 'bob'
+
+As with other data managers, we can commit changes:
+
+.. doctest::
+
+    >>> transaction.commit()
+    >>> dm['name']
+    'bob'
+
+and abort changes:
+
+.. doctest::
+
+    >>> dm['name'] = 'sally'
+    >>> dm['name']
+    'sally'
+    >>> transaction.abort()
+    >>> dm['name']
+    'bob'
+
+Now, let's look at an application that manages funds for people.  It allows
+deposits and debits to be entered for multiple people.  It accepts a sequence
+of entries and generates a sequence of status messages.  For each entry, it
+applies the change and then validates the user's account.  If the user's
+account is invalid, we roll back the change for that entry.  The success or
+failure of an entry is indicated in the output status.  First we'll initialize
+some accounts:
+
+.. doctest::
+
+    >>> dm['bob-balance'] = 0.0
+    >>> dm['bob-credit'] = 0.0
+    >>> dm['sally-balance'] = 0.0
+    >>> dm['sally-credit'] = 100.0
+    >>> transaction.commit()
+
+Now, we'll define a validation function to validate an account:
+
+.. doctest::
+
+    >>> def validate_account(name):
+    ...     if dm[name+'-balance'] + dm[name+'-credit'] < 0:
+    ...         raise ValueError('Overdrawn', name)
+
+And a function to apply entries.  If the function fails in some unexpected
+way, it rolls back all of its changes and prints the error:
+
+.. doctest::
+
+    >>> def apply_entries(entries):
+    ...     savepoint = transaction.savepoint()
+    ...     try:
+    ...         for name, amount in entries:
+    ...             entry_savepoint = transaction.savepoint()
+    ...             try:
+    ...                 dm[name+'-balance'] += amount
+    ...                 validate_account(name)
+    ...             except ValueError as error:
+    ...                 entry_savepoint.rollback()
+    ...                 print("%s %s" % ('Error', str(error)))
+    ...             else:
+    ...                 print("%s %s" % ('Updated', name))
+    ...     except Exception as error:
+    ...         savepoint.rollback()
+    ...         print("%s" % ('Unexpected exception'))
+
+Now let's try applying some entries:
+
+.. doctest::
+
+    >>> apply_entries([
+    ...     ('bob',   10.0),
+    ...     ('sally', 10.0),
+    ...     ('bob',   20.0),
+    ...     ('sally', 10.0),
+    ...     ('bob',   -100.0),
+    ...     ('sally', -100.0),
+    ...     ])
+    Updated bob
+    Updated sally
+    Updated bob
+    Updated sally
+    Error ('Overdrawn', 'bob')
+    Updated sally
+
+    >>> dm['bob-balance']
+    30.0
+
+    >>> dm['sally-balance']
+    -80.0
+
+If we provide entries that cause an unexpected error:
+
+.. doctest::
+
+    >>> apply_entries([
+    ...     ('bob',   10.0),
+    ...     ('sally', 10.0),
+    ...     ('bob',   '20.0'),
+    ...     ('sally', 10.0),
+    ...     ])
+    Updated bob
+    Updated sally
+    Unexpected exception
+
+Because the apply_entries used a savepoint for the entire function, it was
+able to rollback the partial changes without rolling back changes made in the
+previous call to ``apply_entries``:
+
+.. doctest::
+
+    >>> dm['bob-balance']
+    30.0
+
+    >>> dm['sally-balance']
+    -80.0
+
+If we now abort the outer transactions, the earlier changes will go
+away:
+
+.. doctest::
+
+    >>> transaction.abort()
+
+    >>> dm['bob-balance']
+    0.0
+
+    >>> dm['sally-balance']
+    0.0
+
+Savepoint invalidation
+----------------------
+
+A savepoint can be used any number of times:
+
+.. doctest::
+
+    >>> dm['bob-balance'] = 100.0
+    >>> dm['bob-balance']
+    100.0
+    >>> savepoint = transaction.savepoint()
+
+    >>> dm['bob-balance'] = 200.0
+    >>> dm['bob-balance']
+    200.0
+    >>> savepoint.rollback()
+    >>> dm['bob-balance']
+    100.0
+
+    >>> savepoint.rollback()  # redundant, but should be harmless
+    >>> dm['bob-balance']
+    100.0
+
+    >>> dm['bob-balance'] = 300.0
+    >>> dm['bob-balance']
+    300.0
+    >>> savepoint.rollback()
+    >>> dm['bob-balance']
+    100.0
+
+However, using a savepoint invalidates any savepoints that come after it:
+
+.. doctest::
+
+    >>> dm['bob-balance'] = 200.0
+    >>> dm['bob-balance']
+    200.0
+    >>> savepoint1 = transaction.savepoint()
+
+    >>> dm['bob-balance'] = 300.0
+    >>> dm['bob-balance']
+    300.0
+    >>> savepoint2 = transaction.savepoint()
+
+    >>> savepoint.rollback()
+    >>> dm['bob-balance']
+    100.0
+
+    >>> savepoint2.rollback() #doctest: +IGNORE_EXCEPTION_DETAIL
+    Traceback (most recent call last):
+    ...
+    InvalidSavepointRollbackError: invalidated by a later savepoint
+
+    >>> savepoint1.rollback() #doctest: +IGNORE_EXCEPTION_DETAIL
+    Traceback (most recent call last):
+    ...
+    InvalidSavepointRollbackError: invalidated by a later savepoint
+
+    >>> transaction.abort()
+
+
+Databases without savepoint support
+-----------------------------------
+
+Normally it's an error to use savepoints with databases that don't support
+savepoints:
+
+.. doctest::
+
+    >>> dm_no_sp = savepointsample.SampleDataManager()
+    >>> dm_no_sp['name'] = 'bob'
+    >>> transaction.commit()
+    >>> dm_no_sp['name'] = 'sally'
+    >>> transaction.savepoint() #doctest: +IGNORE_EXCEPTION_DETAIL
+    Traceback (most recent call last):
+    ...
+    TypeError: ('Savepoints unsupported', {'name': 'bob'})
+
+    >>> transaction.abort()
+
+However, a flag can be passed to the transaction savepoint method to indicate
+that databases without savepoint support should be tolerated until a savepoint
+is rolled back.  This allows transactions to proceed if there are no reasons
+to roll back:
+
+.. doctest::
+
+    >>> dm_no_sp['name'] = 'sally'
+    >>> savepoint = transaction.savepoint(1)
+    >>> dm_no_sp['name'] = 'sue'
+    >>> transaction.commit()
+    >>> dm_no_sp['name']
+    'sue'
+
+    >>> dm_no_sp['name'] = 'sam'
+    >>> savepoint = transaction.savepoint(1)
+    >>> savepoint.rollback() #doctest: +IGNORE_EXCEPTION_DETAIL
+    Traceback (most recent call last): 
+    ...
+    TypeError: ('Savepoints unsupported', {'name': 'sam'})
+
+
+Failures
+--------
+
+If a failure occurs when creating or rolling back a savepoint, the transaction
+state will be uncertain and the transaction will become uncommitable.  From
+that point on, most transaction operations, including commit, will fail until
+the transaction is aborted.
+
+In the previous example, we got an error when we tried to rollback the
+savepoint.  If we try to commit the transaction, the commit will fail:
+
+.. doctest::
+
+    >>> transaction.commit() #doctest: +IGNORE_EXCEPTION_DETAIL
+    Traceback (most recent call last):
+    ...
+    TransactionFailedError: An operation previously failed, with traceback:
+    ...
+    TypeError: ('Savepoints unsupported', {'name': 'sam'})
+    <BLANKLINE>
+
+We have to abort it to make any progress:
+
+.. doctest::
+
+    >>> transaction.abort()
+
+Similarly, in our earlier example, where we tried to take a savepoint with a
+data manager that didn't support savepoints:
+
+.. doctest::
+
+    >>> dm_no_sp['name'] = 'sally'
+    >>> dm['name'] = 'sally'
+    >>> savepoint = transaction.savepoint() # doctest: +IGNORE_EXCEPTION_DETAIL
+    Traceback (most recent call last):
+    ...
+    TypeError: ('Savepoints unsupported', {'name': 'sue'})
+
+    >>> transaction.commit() # doctest: +IGNORE_EXCEPTION_DETAIL
+    Traceback (most recent call last):
+    ...
+    TransactionFailedError: An operation previously failed, with traceback:
+    ...
+    TypeError: ('Savepoints unsupported', {'name': 'sue'})
+    <BLANKLINE>
+
+    >>> transaction.abort()
+
+After clearing the transaction with an abort, we can get on with new
+transactions:
+
+.. doctest::
+
+    >>> dm_no_sp['name'] = 'sally'
+    >>> dm['name'] = 'sally'
+    >>> transaction.commit()
+    >>> dm_no_sp['name']
+    'sally'
+    >>> dm['name']
+    'sally'
+

Modified: transaction/trunk/setup.py
===================================================================
--- transaction/trunk/setup.py	2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/setup.py	2012-12-18 05:26:55 UTC (rev 128757)
@@ -60,7 +60,7 @@
         'zope.interface',
         ],
       extras_require = {
-        'docs': ['Sphinx'],
+        'docs': ['Sphinx', 'repoze.sphinx.autointerface'],
         'testing': ['nose', 'coverage'],
       },
       entry_points = """\

Copied: transaction/trunk/transaction/_compat.py (from rev 128495, transaction/trunk/transaction/compat.py)
===================================================================
--- transaction/trunk/transaction/_compat.py	                        (rev 0)
+++ transaction/trunk/transaction/_compat.py	2012-12-18 05:26:55 UTC (rev 128757)
@@ -0,0 +1,90 @@
+import sys
+import types
+
+PY3 = sys.version_info[0] == 3
+
+if PY3: # pragma: no cover
+    string_types = str,
+    integer_types = int,
+    class_types = type,
+    text_type = str
+    binary_type = bytes
+    long = int
+else:
+    string_types = basestring,
+    integer_types = (int, long)
+    class_types = (type, types.ClassType)
+    text_type = unicode
+    binary_type = str
+    long = long
+
+def bytes_(s, encoding='latin-1', errors='strict'): #pragma NO COVER
+    if isinstance(s, text_type):
+        return s.encode(encoding, errors)
+    return s
+
+if PY3: # pragma: no cover
+    def native_(s, encoding='latin-1', errors='strict'): #pragma NO COVER
+        if isinstance(s, text_type):
+            return s
+        return str(s, encoding, errors)
+else:
+    def native_(s, encoding='latin-1', errors='strict'): #pragma NO COVER
+        if isinstance(s, text_type):
+            return s.encode(encoding, errors)
+        return str(s)
+
+if PY3: #pragma NO COVER
+    from io import StringIO
+else:
+    from io import BytesIO as StringIO
+
+if PY3: #pragma NO COVER
+    from collections import MutableMapping
+else:
+    from UserDict import UserDict as MutableMapping
+
+if PY3: # pragma: no cover
+    import builtins
+    exec_ = getattr(builtins, "exec")
+
+
+    def reraise(tp, value, tb=None): #pragma NO COVER
+        if value.__traceback__ is not tb:
+            raise value.with_traceback(tb)
+        raise value
+
+else: # pragma: no cover
+    def exec_(code, globs=None, locs=None): #pragma NO COVER
+        """Execute code in a namespace."""
+        if globs is None:
+            frame = sys._getframe(1)
+            globs = frame.f_globals
+            if locs is None:
+                locs = frame.f_locals
+            del frame
+        elif locs is None:
+            locs = globs
+        exec("""exec code in globs, locs""")
+
+    exec_("""def reraise(tp, value, tb=None):
+    raise tp, value, tb
+""")
+
+
+if PY3: #pragma NO COVER
+    try:
+        from threading import get_ident as get_thread_ident
+    except ImportError:
+        from threading import _get_ident as get_thread_ident
+else:
+    from thread import get_ident as get_thread_ident
+    
+    
+if PY3:
+    def func_name(func): #pragma NO COVER
+        return func.__name__
+else:
+    def func_name(func): #pragma NO COVER
+        return func.func_name
+ 

Modified: transaction/trunk/transaction/_manager.py
===================================================================
--- transaction/trunk/transaction/_manager.py	2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/_manager.py	2012-12-18 05:26:55 UTC (rev 128757)
@@ -21,11 +21,11 @@
 
 from zope.interface import implementer
 
+from transaction.interfaces import ITransactionManager
+from transaction.interfaces import TransientError
 from transaction.weakset import WeakSet
+from transaction._compat import reraise
 from transaction._transaction import Transaction
-from transaction.interfaces import ITransactionManager
-from transaction.interfaces import TransientError
-from transaction.compat import reraise
 
 
 # We have to remember sets of synch objects, especially Connections.
@@ -54,6 +54,7 @@
 # so that Transactions "see" synchronizers that get registered after the
 # Transaction object is constructed.
 
+
 @implementer(ITransactionManager)
 class TransactionManager(object):
 
@@ -80,7 +81,8 @@
         return self._txn
 
     def free(self, txn):
-        assert txn is self._txn
+        if txn is not self._txn:
+            raise ValueError("Foreign transaction")
         self._txn = None
 
     def registerSynch(self, synch):
@@ -125,7 +127,8 @@
         return self.get().savepoint(optimistic)
 
     def attempts(self, number=3):
-        assert number > 0
+        if number <= 0:
+            raise ValueError("number must be positive")
         while number:
             number -= 1
             if number:
@@ -149,6 +152,7 @@
     Each thread is associated with a unique transaction.
     """
 
+
 class Attempt(object):
 
     def __init__(self, manager):
@@ -160,7 +164,7 @@
         if retry:
             return retry # suppress the exception if necessary
         reraise(t, v, tb) # otherwise reraise the exception
-        
+
     def __enter__(self):
         return self.manager.__enter__()
 
@@ -172,4 +176,3 @@
                 return self._retry_or_raise(*sys.exc_info())
         else:
             return self._retry_or_raise(t, v, tb)
-        

Modified: transaction/trunk/transaction/_transaction.py
===================================================================
--- transaction/trunk/transaction/_transaction.py	2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/_transaction.py	2012-12-18 05:26:55 UTC (rev 128757)
@@ -11,92 +11,6 @@
 # FOR A PARTICULAR PURPOSE.
 #
 ############################################################################
-"""Transaction objects manage resources for an individual activity.
-
-Compatibility issues
---------------------
-
-The implementation of Transaction objects involves two layers of
-backwards compatibility, because this version of transaction supports
-both ZODB 3 and ZODB 4.  Zope is evolving towards the ZODB4
-interfaces.
-
-Transaction has two methods for a resource manager to call to
-participate in a transaction -- register() and join().  join() takes a
-resource manager and adds it to the list of resources.  register() is
-for backwards compatibility.  It takes a persistent object and
-registers its _p_jar attribute.  TODO: explain adapter
-
-Two-phase commit
-----------------
-
-A transaction commit involves an interaction between the transaction
-object and one or more resource managers.  The transaction manager
-calls the following four methods on each resource manager; it calls
-tpc_begin() on each resource manager before calling commit() on any of
-them.
-
-    1. tpc_begin(txn)
-    2. commit(txn)
-    3. tpc_vote(txn)
-    4. tpc_finish(txn)
-
-Before-commit hook
-------------------
-
-Sometimes, applications want to execute some code when a transaction is
-committed.  For example, one might want to delay object indexing until a
-transaction commits, rather than indexing every time an object is changed.
-Or someone might want to check invariants only after a set of operations.  A
-pre-commit hook is available for such use cases:  use addBeforeCommitHook(),
-passing it a callable and arguments.  The callable will be called with its
-arguments at the start of the commit (but not for substransaction commits).
-
-After-commit hook
-------------------
-
-Sometimes, applications want to execute code after a transaction commit
-attempt succeeds or aborts. For example, one might want to launch non
-transactional code after a successful commit. Or still someone might
-want to launch asynchronous code after.  A post-commit hook is
-available for such use cases: use addAfterCommitHook(), passing it a
-callable and arguments.  The callable will be called with a Boolean
-value representing the status of the commit operation as first
-argument (true if successfull or false iff aborted) preceding its
-arguments at the start of the commit (but not for substransaction
-commits). Commit hooks are not called for transaction.abort().
-
-Error handling
---------------
-
-When errors occur during two-phase commit, the transaction manager
-aborts all the resource managers.  The specific methods it calls
-depend on whether the error occurs before or after the call to
-tpc_vote() on that transaction manager.
-
-If the resource manager has not voted, then the resource manager will
-have one or more uncommitted objects.  There are two cases that lead
-to this state; either the transaction manager has not called commit()
-for any objects on this resource manager or the call that failed was a
-commit() for one of the objects of this resource manager.  For each
-uncommitted object, including the object that failed in its commit(),
-call abort().
-
-Once uncommitted objects are aborted, tpc_abort() or abort_sub() is
-called on each resource manager.
-
-Synchronization
----------------
-
-You can register sychronization objects (synchronizers) with the
-tranasction manager.  The synchronizer must implement
-beforeCompletion() and afterCompletion() methods.  The transaction
-manager calls beforeCompletion() when it starts a top-level two-phase
-commit.  It calls afterCompletion() when a top-level transaction is
-committed or aborted.  The methods are passed the current Transaction
-as their only argument.
-"""
-
 import binascii
 import logging
 import sys
@@ -105,17 +19,30 @@
 
 from zope.interface import implementer
 
-from transaction.compat import reraise
-from transaction.compat import get_thread_ident
-from transaction.compat import native_
-from transaction.compat import bytes_
-from transaction.compat import StringIO
 from transaction.weakset import WeakSet
 from transaction.interfaces import TransactionFailedError
 from transaction import interfaces
+from transaction._compat import reraise
+from transaction._compat import get_thread_ident
+from transaction._compat import native_
+from transaction._compat import bytes_
+from transaction._compat import StringIO
 
 _marker = object()
 
+_TB_BUFFER = None #unittests may hook
+def _makeTracebackBuffer(): #pragma NO COVER
+    if _TB_BUFFER is not None:
+        return _TB_BUFFER
+    return StringIO()
+
+_LOGGER = None #unittests may hook
+def _makeLogger(): #pragma NO COVER
+    if _LOGGER is not None:
+        return _LOGGER
+    return logging.getLogger("txn.%d" % get_thread_ident())
+    
+
 # The point of this is to avoid hiding exceptions (which the builtin
 # hasattr() does).
 def myhasattr(obj, attr):
@@ -177,7 +104,7 @@
         # directly by storages, leading underscore notwithstanding.
         self._extension = {}
 
-        self.log = logging.getLogger("txn.%d" % get_thread_ident())
+        self.log = _makeLogger()
         self.log.debug("new transaction")
 
         # If a commit fails, the traceback is saved in _failure_traceback.
@@ -203,7 +130,7 @@
             if self.status is not Status.ACTIVE:
                 # should not doom transactions in the middle,
                 # or after, a commit
-                raise AssertionError()
+                raise ValueError('non-doomable')
             self.status = Status.DOOMED
 
     # Raise TransactionFailedError, due to commit()/join()/register()
@@ -307,7 +234,6 @@
         # be stored when the transaction commits.  For other
         # objects, the object implements the standard two-phase
         # commit protocol.
-
         manager = getattr(obj, "_p_jar", obj)
         if manager is None:
             raise ValueError("Register with no manager")
@@ -364,7 +290,7 @@
     def _saveAndGetCommitishError(self):
         self.status = Status.COMMITFAILED
         # Save the traceback for TransactionFailedError.
-        ft = self._failure_traceback = StringIO()
+        ft = self._failure_traceback = _makeTracebackBuffer()
         t = None
         v = None
         tb = None
@@ -379,7 +305,6 @@
             return t, v, tb
         finally:
             del t, v, tb
-        
 
     def _saveAndRaiseCommitishError(self):
         t = None
@@ -390,7 +315,6 @@
             reraise(t, v, tb)
         finally:
             del t, v, tb
-            
 
     def getBeforeCommitHooks(self):
         """ See ITransaction.
@@ -566,6 +490,7 @@
 
 # TODO: We need a better name for the adapters.
 
+
 class MultiObjectResourceAdapter(object):
     """Adapt the old-style register() call to the new-style join().
 
@@ -573,7 +498,6 @@
     the transaction manager.  With register(), an individual object
     is passed to register().
     """
-
     def __init__(self, jar):
         self.manager = jar
         self.objects = []
@@ -624,6 +548,7 @@
         finally:
             del t, v, tb
 
+
 def rm_key(rm):
     func = getattr(rm, 'sortKey', None)
     if func is not None:
@@ -634,13 +559,14 @@
 
     This function does not raise an exception.
     """
-
     # We should always be able to get __class__.
     klass = o.__class__.__name__
-    # oid would be great, but may this isn't a persistent object.
+    # oid would be great, but maybe this isn't a persistent object.
     oid = getattr(o, "_p_oid", _marker)
     if oid is not _marker:
         oid = oid_repr(oid)
+    else:
+        oid = 'None'
     return "%s oid=%s" % (klass, oid)
 
 def oid_repr(oid):
@@ -657,6 +583,7 @@
     else:
         return repr(oid)
 
+
 # TODO: deprecate for 3.6.
 class DataManagerAdapter(object):
     """Adapt zodb 4-style data managers to zodb3 style
@@ -700,6 +627,7 @@
     def sortKey(self):
         return self._datamanager.sortKey()
 
+
 @implementer(interfaces.ISavepoint)
 class Savepoint:
     """Transaction savepoint.
@@ -742,6 +670,7 @@
             # Mark the transaction as failed.
             transaction._saveAndRaiseCommitishError() # reraises!
 
+
 class AbortSavepoint:
 
     def __init__(self, datamanager, transaction):
@@ -752,6 +681,7 @@
         self.datamanager.abort(self.transaction)
         self.transaction._unjoin(self.datamanager)
 
+
 class NoRollbackSavepoint:
 
     def __init__(self, datamanager):

Deleted: transaction/trunk/transaction/compat.py
===================================================================
--- transaction/trunk/transaction/compat.py	2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/compat.py	2012-12-18 05:26:55 UTC (rev 128757)
@@ -1,95 +0,0 @@
-import sys
-import types
-
-PY3 = sys.version_info[0] == 3
-
-if PY3: # pragma: no cover
-    string_types = str,
-    integer_types = int,
-    class_types = type,
-    text_type = str
-    binary_type = bytes
-    long = int
-else:
-    string_types = basestring,
-    integer_types = (int, long)
-    class_types = (type, types.ClassType)
-    text_type = unicode
-    binary_type = str
-    long = long
-
-def text_(s, encoding='latin-1', errors='strict'):
-    if isinstance(s, binary_type):
-        return s.decode(encoding, errors)
-    return s # pragma: no cover
-
-def bytes_(s, encoding='latin-1', errors='strict'):
-    if isinstance(s, text_type):
-        return s.encode(encoding, errors)
-    return s
-
-if PY3: # pragma: no cover
-    def native_(s, encoding='latin-1', errors='strict'):
-        if isinstance(s, text_type):
-            return s
-        return str(s, encoding, errors)
-else:
-    def native_(s, encoding='latin-1', errors='strict'):
-        if isinstance(s, text_type):
-            return s.encode(encoding, errors)
-        return str(s)
-
-if PY3:
-    from io import StringIO
-else:
-    from io import BytesIO as StringIO
-
-if PY3:
-    from collections import MutableMapping
-else:
-    from UserDict import UserDict as MutableMapping
-
-if PY3: # pragma: no cover
-    import builtins
-    exec_ = getattr(builtins, "exec")
-
-
-    def reraise(tp, value, tb=None):
-        if value.__traceback__ is not tb:
-            raise value.with_traceback(tb)
-        raise value
-
-else: # pragma: no cover
-    def exec_(code, globs=None, locs=None):
-        """Execute code in a namespace."""
-        if globs is None:
-            frame = sys._getframe(1)
-            globs = frame.f_globals
-            if locs is None:
-                locs = frame.f_locals
-            del frame
-        elif locs is None:
-            locs = globs
-        exec("""exec code in globs, locs""")
-
-    exec_("""def reraise(tp, value, tb=None):
-    raise tp, value, tb
-""")
-
-
-if PY3:
-    try:
-        from threading import get_ident as get_thread_ident
-    except ImportError:
-        from threading import _get_ident as get_thread_ident
-else:
-    from thread import get_ident as get_thread_ident
-    
-    
-if PY3:
-    def func_name(func):
-        return func.__name__
-else:
-    def func_name(func):
-        return func.func_name
-    

Copied: transaction/trunk/transaction/tests/common.py (from rev 128756, transaction/branches/sphinx/transaction/tests/common.py)
===================================================================
--- transaction/trunk/transaction/tests/common.py	                        (rev 0)
+++ transaction/trunk/transaction/tests/common.py	2012-12-18 05:26:55 UTC (rev 128757)
@@ -0,0 +1,65 @@
+##############################################################################
+#
+# Copyright (c) 2012 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+
+
+class DummyFile(object):
+    def __init__(self):
+        self._lines = []
+    def write(self, text):
+        self._lines.append(text)
+    def writelines(self, lines):
+        self._lines.extend(lines)
+
+
+class DummyLogger(object):
+    def __init__(self):
+        self._clear()
+    def _clear(self):
+        self._log = []
+    def log(self, level, msg, *args, **kw):
+        if args:
+            self._log.append((level, msg % args))
+        elif kw:
+            self._log.append((level, msg % kw))
+        else:
+            self._log.append((level, msg))
+    def debug(self, msg, *args, **kw):
+        self.log('debug', msg, *args, **kw)
+    def error(self, msg, *args, **kw):
+        self.log('error', msg, *args, **kw)
+    def critical(self, msg, *args, **kw):
+        self.log('critical', msg, *args, **kw)
+
+
+class Monkey(object):
+    # context-manager for replacing module names in the scope of a test.
+    def __init__(self, module, **kw):
+        self.module = module
+        self.to_restore = dict([(key, getattr(module, key)) for key in kw])
+        for key, value in kw.items():
+            setattr(module, key, value)
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        for key, value in self.to_restore.items():
+            setattr(self.module, key, value)
+
+def assertRaisesEx(e_type, checked, *args, **kw):
+    try:
+        checked(*args, **kw)
+    except e_type as e:
+        return e
+    raise AssertionError("Didn't raise: %s" % e_type.__name__)

Deleted: transaction/trunk/transaction/tests/convenience.txt
===================================================================
--- transaction/trunk/transaction/tests/convenience.txt	2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/tests/convenience.txt	2012-12-18 05:26:55 UTC (rev 128757)
@@ -1,165 +0,0 @@
-Transaction convenience support
-===============================
-
-(We *really* need to write proper documentation for the transaction
- package, but I don't want to block the conveniences documented here
- for that.)
-
-with support
-------------
-
-We can now use the with statement to define transaction boundaries.
-
-    >>> import transaction.tests.savepointsample
-    >>> dm = transaction.tests.savepointsample.SampleSavepointDataManager()
-    >>> list(dm.keys())
-    []
-
-We can use it with a manager:
-
-    >>> with transaction.manager as t:
-    ...     dm['z'] = 3
-    ...     t.note('test 3')
-
-    >>> dm['z']
-    3
-
-    >>> dm.last_note
-    'test 3'
-
-    >>> with transaction.manager: #doctest ELLIPSIS
-    ...     dm['z'] = 4
-    ...     xxx
-    Traceback (most recent call last):
-    ...
-    NameError: ... name 'xxx' is not defined
-
-    >>> dm['z']
-    3
-
-On Python 2, you can also abbreviate ``with transaction.manager:`` as ``with
-transaction:``.  This does not work on Python 3 (see see
-http://bugs.python.org/issue12022).
-
-Retries
--------
-
-Commits can fail for transient reasons, especially conflicts.
-Applications will often retry transactions some number of times to
-overcome transient failures.  This typically looks something like::
-
-    for i in range(3):
-        try:
-           with transaction.manager:
-               ... some something ...
-        except SomeTransientException:
-           contine
-        else:
-           break
-
-This is rather ugly.
-
-Transaction managers provide a helper for this case. To show this,
-we'll use a contrived example:
-
-
-    >>> ntry = 0
-    >>> with transaction.manager:
-    ...      dm['ntry'] = 0
-
-    >>> import transaction.interfaces
-    >>> class Retry(transaction.interfaces.TransientError):
-    ...     pass
-
-    >>> for attempt in transaction.manager.attempts():
-    ...     with attempt as t:
-    ...         t.note('test')
-    ...         print("%s %s" % (dm['ntry'], ntry))
-    ...         ntry += 1
-    ...         dm['ntry'] = ntry
-    ...         if ntry % 3:
-    ...             raise Retry(ntry)
-    0 0
-    0 1
-    0 2
-
-The raising of a subclass of TransientError is critical here. It's
-what signals that the transaction should be retried.  It is generally
-up to the data manager to signal that a transaction should try again
-by raising a subclass of TransientError (or TransientError itself, of
-course).
-
-You shouldn't make any assumptions about the object returned by the
-iterator.  (It isn't a transaction or transaction manager, as far as
-you know. :)  If you use the ``as`` keyword in the ``with`` statement,
-a transaction object will be assigned to the variable named.
-
-By default, it tries 3 times. We can tell it how many times to try:
-
-    >>> for attempt in transaction.manager.attempts(2):
-    ...     with attempt:
-    ...         ntry += 1
-    ...         if ntry % 3:
-    ...             raise Retry(ntry)
-    Traceback (most recent call last):
-    ...
-    Retry: 5
-
-It it doesn't succeed in that many times, the exception will be
-propagated.
-
-Of course, other errors are propagated directly:
-
-    >>> ntry = 0
-    >>> for attempt in transaction.manager.attempts():
-    ...     with attempt:
-    ...         ntry += 1
-    ...         if ntry == 3:
-    ...             raise ValueError(ntry)
-    Traceback (most recent call last):
-    ...
-    ValueError: 3
-
-We can use the default transaction manager:
-
-    >>> for attempt in transaction.attempts():
-    ...     with attempt as t:
-    ...         t.note('test')
-    ...         print("%s %s" % (dm['ntry'], ntry))
-    ...         ntry += 1
-    ...         dm['ntry'] = ntry
-    ...         if ntry % 3:
-    ...             raise Retry(ntry)
-    3 3
-    3 4
-    3 5
-
-Sometimes, a data manager doesn't raise exceptions directly, but
-wraps other other systems that raise exceptions outside of it's
-control.  Data  managers can provide a should_retry method that takes
-an exception instance and returns True if the transaction should be
-attempted again.
-
-    >>> class DM(transaction.tests.savepointsample.SampleSavepointDataManager):
-    ...     def should_retry(self, e):
-    ...         if 'should retry' in str(e):
-    ...             return True
-
-    >>> ntry = 0
-    >>> dm2 = DM()
-    >>> with transaction.manager:
-    ...     dm2['ntry'] = 0
-    >>> for attempt in transaction.manager.attempts():
-    ...     with attempt:
-    ...         print("%s %s" % (dm['ntry'], ntry))
-    ...         ntry += 1
-    ...         dm['ntry'] = ntry
-    ...         dm2['ntry'] = ntry
-    ...         if ntry % 3:
-    ...             raise ValueError('we really should retry this')
-    6 0
-    6 1
-    6 2
-
-    >>> dm2['ntry']
-    3

Deleted: transaction/trunk/transaction/tests/doom.txt
===================================================================
--- transaction/trunk/transaction/tests/doom.txt	2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/tests/doom.txt	2012-12-18 05:26:55 UTC (rev 128757)
@@ -1,136 +0,0 @@
-Dooming Transactions
-====================
-
-A doomed transaction behaves exactly the same way as an active transaction but
-raises an error on any attempt to commit it, thus forcing an abort.
-
-Doom is useful in places where abort is unsafe and an exception cannot be
-raised.  This occurs when the programmer wants the code following the doom to
-run but not commit. It is unsafe to abort in these circumstances as a following
-get() may implicitly open a new transaction.
-
-Any attempt to commit a doomed transaction will raise a DoomedTransaction
-exception.
-
-An example of such a use case can be found in
-zope/app/form/browser/editview.py.  Here a form validation failure must doom
-the transaction as committing the transaction may have side-effects. However,
-the form code must continue to calculate a form containing the error messages
-to return.
-
-For Zope in general, code running within a request should always doom
-transactions rather than aborting them. It is the responsibilty of the
-publication to either abort() or commit() the transaction. Application code can
-use savepoints and doom() safely.
-
-To see how it works we first need to create a stub data manager:
-
-    >>> from transaction.interfaces import IDataManager
-    >>> from zope.interface import implementer
-    >>> @implementer(IDataManager)
-    ... class DataManager:
-    ...     def __init__(self):
-    ...         self.attr_counter = {}
-    ...     def __getattr__(self, name):
-    ...         def f(transaction):
-    ...             self.attr_counter[name] = self.attr_counter.get(name, 0) + 1
-    ...         return f
-    ...     def total(self):
-    ...         count = 0
-    ...         for access_count in self.attr_counter.values():
-    ...             count += access_count
-    ...         return count
-    ...     def sortKey(self):
-    ...         return 1
-
-Start a new transaction:
-
-    >>> import transaction
-    >>> txn = transaction.begin()
-    >>> dm = DataManager()
-    >>> txn.join(dm)
-
-We can ask a transaction if it is doomed to avoid expensive operations. An
-example of a use case is an object-relational mapper where a pre-commit hook
-sends all outstanding SQL to a relational database for objects changed during
-the transaction. This expensive operation is not necessary if the transaction
-has been doomed. A non-doomed transaction should return False:
-
-    >>> txn.isDoomed()
-    False
-
-We can doom a transaction by calling .doom() on it:
-
-    >>> txn.doom()
-    >>> txn.isDoomed()
-    True
-
-We can doom it again if we like:
-
-    >>> txn.doom()
-
-The data manager is unchanged at this point:
-
-    >>> dm.total()
-    0
-
-Attempting to commit a doomed transaction any number of times raises a
-DoomedTransaction:
-
-    >>> txn.commit() # doctest: +IGNORE_EXCEPTION_DETAIL
-    Traceback (most recent call last):
-    DoomedTransaction: transaction doomed, cannot commit
-    >>> txn.commit() # doctest: +IGNORE_EXCEPTION_DETAIL
-    Traceback (most recent call last):
-    DoomedTransaction: transaction doomed, cannot commit
-
-But still leaves the data manager unchanged:
-
-    >>> dm.total()
-    0
-
-But the doomed transaction can be aborted:
-
-    >>> txn.abort()
-
-Which aborts the data manager:
-
-    >>> dm.total()
-    1
-    >>> dm.attr_counter['abort']
-    1
-
-Dooming the current transaction can also be done directly from the transaction
-module. We can also begin a new transaction directly after dooming the old one:
-
-    >>> txn = transaction.begin()
-    >>> transaction.isDoomed()
-    False
-    >>> transaction.doom()
-    >>> transaction.isDoomed()
-    True
-    >>> txn = transaction.begin()
-
-After committing a transaction we get an assertion error if we try to doom the
-transaction. This could be made more specific, but trying to doom a transaction
-after it's been committed is probably a programming error:
-
-    >>> txn = transaction.begin()
-    >>> txn.commit()
-    >>> txn.doom()
-    Traceback (most recent call last):
-        ...
-    AssertionError
-
-A doomed transaction should act the same as an active transaction, so we should
-be able to join it:
-
-    >>> txn = transaction.begin()
-    >>> txn.doom()
-    >>> dm2 = DataManager()
-    >>> txn.join(dm2)
-
-Clean up:
-
-    >>> txn = transaction.begin()
-    >>> txn.abort()

Copied: transaction/trunk/transaction/tests/examples.py (from rev 128756, transaction/branches/sphinx/transaction/tests/examples.py)
===================================================================
--- transaction/trunk/transaction/tests/examples.py	                        (rev 0)
+++ transaction/trunk/transaction/tests/examples.py	2012-12-18 05:26:55 UTC (rev 128757)
@@ -0,0 +1,181 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Sample objects for use in tests
+
+"""
+
+
+class DataManager(object):
+    """Sample data manager
+
+    Used by the 'datamanager' chapter in the Sphinx docs.
+    """
+    def __init__(self):
+        self.state = 0
+        self.sp = 0
+        self.transaction = None
+        self.delta = 0
+        self.prepared = False
+
+    def inc(self, n=1):
+        self.delta += n
+
+    def prepare(self, transaction):
+        if self.prepared:
+            raise TypeError('Already prepared')
+        self._checkTransaction(transaction)
+        self.prepared = True
+        self.transaction = transaction
+        self.state += self.delta
+
+    def _checkTransaction(self, transaction):
+        if (transaction is not self.transaction
+            and self.transaction is not None):
+            raise TypeError("Transaction missmatch",
+                            transaction, self.transaction)
+
+    def abort(self, transaction):
+        self._checkTransaction(transaction)
+        if self.transaction is not None:
+            self.transaction = None
+
+        if self.prepared:
+            self.state -= self.delta
+            self.prepared = False
+
+        self.delta = 0
+
+    def commit(self, transaction):
+        if not self.prepared:
+            raise TypeError('Not prepared to commit')
+        self._checkTransaction(transaction)
+        self.delta = 0
+        self.transaction = None
+        self.prepared = False
+
+    def savepoint(self, transaction):
+        if self.prepared:
+            raise TypeError("Can't get savepoint during two-phase commit")
+        self._checkTransaction(transaction)
+        self.transaction = transaction
+        self.sp += 1
+        return Rollback(self)
+
+
+class Rollback(object):
+
+    def __init__(self, dm):
+        self.dm = dm
+        self.sp = dm.sp
+        self.delta = dm.delta
+        self.transaction = dm.transaction
+
+    def rollback(self):
+        if self.transaction is not self.dm.transaction:
+            raise TypeError("Attempt to rollback stale rollback")
+        if self.dm.sp < self.sp:
+            raise TypeError("Attempt to roll back to invalid save point",
+                            self.sp, self.dm.sp)
+        self.dm.sp = self.sp
+        self.dm.delta = self.delta
+
+
+class ResourceManager(object):
+    """ Sample resource manager.
+
+    Used by the 'resourcemanager' chapter in the Sphinx docs.
+    """
+    def __init__(self):
+        self.state = 0
+        self.sp = 0
+        self.transaction = None
+        self.delta = 0
+        self.txn_state = None
+
+    def _check_state(self, *ok_states):
+        if self.txn_state not in ok_states:
+            raise ValueError("txn in state %r but expected one of %r" %
+                             (self.txn_state, ok_states))
+
+    def _checkTransaction(self, transaction):
+        if (transaction is not self.transaction
+            and self.transaction is not None):
+            raise TypeError("Transaction missmatch",
+                            transaction, self.transaction)
+
+    def inc(self, n=1):
+        self.delta += n
+
+    def tpc_begin(self, transaction):
+        self._checkTransaction(transaction)
+        self._check_state(None)
+        self.transaction = transaction
+        self.txn_state = 'tpc_begin'
+
+    def tpc_vote(self, transaction):
+        self._checkTransaction(transaction)
+        self._check_state('tpc_begin')
+        self.state += self.delta
+        self.txn_state = 'tpc_vote'
+
+    def tpc_finish(self, transaction):
+        self._checkTransaction(transaction)
+        self._check_state('tpc_vote')
+        self.delta = 0
+        self.transaction = None
+        self.prepared = False
+        self.txn_state = None
+
+    def tpc_abort(self, transaction):
+        self._checkTransaction(transaction)
+        if self.transaction is not None:
+            self.transaction = None
+
+        if self.txn_state == 'tpc_vote':
+            self.state -= self.delta
+
+        self.txn_state = None
+        self.delta = 0
+
+    def savepoint(self, transaction):
+        if self.txn_state is not None:
+            raise TypeError("Can't get savepoint during two-phase commit")
+        self._checkTransaction(transaction)
+        self.transaction = transaction
+        self.sp += 1
+        return SavePoint(self)
+
+    def discard(self, transaction):
+        pass
+
+
+class SavePoint(object):
+
+    def __init__(self, rm):
+        self.rm = rm
+        self.sp = rm.sp
+        self.delta = rm.delta
+        self.transaction = rm.transaction
+
+    def rollback(self):
+        if self.transaction is not self.rm.transaction:
+            raise TypeError("Attempt to rollback stale rollback")
+        if self.rm.sp < self.sp:
+            raise TypeError("Attempt to roll back to invalid save point",
+                            self.sp, self.rm.sp)
+        self.rm.sp = self.sp
+        self.rm.delta = self.delta
+
+    def discard(self):
+        pass

Deleted: transaction/trunk/transaction/tests/sampledm.py
===================================================================
--- transaction/trunk/transaction/tests/sampledm.py	2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/tests/sampledm.py	2012-12-18 05:26:55 UTC (rev 128757)
@@ -1,412 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2004 Zope Foundation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE.
-#
-##############################################################################
-"""Sample objects for use in tests
-
-$Id: sampledm.py 29896 2005-04-07 04:48:06Z tim_one $
-"""
-
-class DataManager(object):
-    """Sample data manager
-
-       This class provides a trivial data-manager implementation and doc
-       strings to illustrate the the protocol and to provide a tool for
-       writing tests.
-
-       Our sample data manager has state that is updated through an inc
-       method and through transaction operations.
-
-       When we create a sample data manager:
-
-       >>> dm = DataManager()
-
-       It has two bits of state, state:
-
-       >>> dm.state
-       0
-
-       and delta:
-
-       >>> dm.delta
-       0
-
-       Both of which are initialized to 0.  state is meant to model
-       committed state, while delta represents tentative changes within a
-       transaction.  We change the state by calling inc:
-
-       >>> dm.inc()
-
-       which updates delta:
-
-       >>> dm.delta
-       1
-
-       but state isn't changed until we commit the transaction:
-
-       >>> dm.state
-       0
-
-       To commit the changes, we use 2-phase commit. We execute the first
-       stage by calling prepare.  We need to pass a transation. Our
-       sample data managers don't really use the transactions for much,
-       so we'll be lazy and use strings for transactions:
-
-       >>> t1 = '1'
-       >>> dm.prepare(t1)
-
-       The sample data manager updates the state when we call prepare:
-
-       >>> dm.state
-       1
-       >>> dm.delta
-       1
-
-       This is mainly so we can detect some affect of calling the methods.
-
-       Now if we call commit:
-
-       >>> dm.commit(t1)
-
-       Our changes are"permanent".  The state reflects the changes and the
-       delta has been reset to 0.
-
-       >>> dm.state
-       1
-       >>> dm.delta
-       0
-       """
-
-    def __init__(self):
-        self.state = 0
-        self.sp = 0
-        self.transaction = None
-        self.delta = 0
-        self.prepared = False
-
-    def inc(self, n=1):
-        self.delta += n
-
-    def prepare(self, transaction):
-        """Prepare to commit data
-
-        >>> dm = DataManager()
-        >>> dm.inc()
-        >>> t1 = '1'
-        >>> dm.prepare(t1)
-        >>> dm.commit(t1)
-        >>> dm.state
-        1
-        >>> dm.inc()
-        >>> t2 = '2'
-        >>> dm.prepare(t2)
-        >>> dm.abort(t2)
-        >>> dm.state
-        1
-
-        It is en error to call prepare more than once without an intervening
-        commit or abort:
-
-        >>> dm.prepare(t1)
-
-        >>> dm.prepare(t1)
-        Traceback (most recent call last):
-        ...
-        TypeError: Already prepared
-
-        >>> dm.prepare(t2)
-        Traceback (most recent call last):
-        ...
-        TypeError: Already prepared
-
-        >>> dm.abort(t1)
-
-        If there was a preceeding savepoint, the transaction must match:
-
-        >>> rollback = dm.savepoint(t1)
-        >>> dm.prepare(t2)
-        Traceback (most recent call last):
-        ,,,
-        TypeError: ('Transaction missmatch', '2', '1')
-
-        >>> dm.prepare(t1)
-
-        """
-        if self.prepared:
-            raise TypeError('Already prepared')
-        self._checkTransaction(transaction)
-        self.prepared = True
-        self.transaction = transaction
-        self.state += self.delta
-
-    def _checkTransaction(self, transaction):
-        if (transaction is not self.transaction
-            and self.transaction is not None):
-            raise TypeError("Transaction missmatch",
-                            transaction, self.transaction)
-
-    def abort(self, transaction):
-        """Abort a transaction
-
-        The abort method can be called before two-phase commit to
-        throw away work done in the transaction:
-
-        >>> dm = DataManager()
-        >>> dm.inc()
-        >>> dm.state, dm.delta
-        (0, 1)
-        >>> t1 = '1'
-        >>> dm.abort(t1)
-        >>> dm.state, dm.delta
-        (0, 0)
-
-        The abort method also throws away work done in savepoints:
-
-        >>> dm.inc()
-        >>> r = dm.savepoint(t1)
-        >>> dm.inc()
-        >>> r = dm.savepoint(t1)
-        >>> dm.state, dm.delta
-        (0, 2)
-        >>> dm.abort(t1)
-        >>> dm.state, dm.delta
-        (0, 0)
-
-        If savepoints are used, abort must be passed the same
-        transaction:
-
-        >>> dm.inc()
-        >>> r = dm.savepoint(t1)
-        >>> t2 = '2'
-        >>> dm.abort(t2)
-        Traceback (most recent call last):
-        ...
-        TypeError: ('Transaction missmatch', '2', '1')
-
-        >>> dm.abort(t1)
-
-        The abort method is also used to abort a two-phase commit:
-
-        >>> dm.inc()
-        >>> dm.state, dm.delta
-        (0, 1)
-        >>> dm.prepare(t1)
-        >>> dm.state, dm.delta
-        (1, 1)
-        >>> dm.abort(t1)
-        >>> dm.state, dm.delta
-        (0, 0)
-
-        Of course, the transactions passed to prepare and abort must
-        match:
-
-        >>> dm.prepare(t1)
-        >>> dm.abort(t2)
-        Traceback (most recent call last):
-        ...
-        TypeError: ('Transaction missmatch', '2', '1')
-
-        >>> dm.abort(t1)
-
-
-        """
-        self._checkTransaction(transaction)
-        if self.transaction is not None:
-            self.transaction = None
-
-        if self.prepared:
-            self.state -= self.delta
-            self.prepared = False
-
-        self.delta = 0
-
-    def commit(self, transaction):
-        """Complete two-phase commit
-
-        >>> dm = DataManager()
-        >>> dm.state
-        0
-        >>> dm.inc()
-
-        We start two-phase commit by calling prepare:
-
-        >>> t1 = '1'
-        >>> dm.prepare(t1)
-
-        We complete it by calling commit:
-
-        >>> dm.commit(t1)
-        >>> dm.state
-        1
-
-        It is an error ro call commit without calling prepare first:
-
-        >>> dm.inc()
-        >>> t2 = '2'
-        >>> dm.commit(t2)
-        Traceback (most recent call last):
-        ...
-        TypeError: Not prepared to commit
-
-        >>> dm.prepare(t2)
-        >>> dm.commit(t2)
-
-        If course, the transactions given to prepare and commit must
-        be the same:
-
-        >>> dm.inc()
-        >>> t3 = '3'
-        >>> dm.prepare(t3)
-        >>> dm.commit(t2)
-        Traceback (most recent call last):
-        ...
-        TypeError: ('Transaction missmatch', '2', '3')
-
-        """
-        if not self.prepared:
-            raise TypeError('Not prepared to commit')
-        self._checkTransaction(transaction)
-        self.delta = 0
-        self.transaction = None
-        self.prepared = False
-
-    def savepoint(self, transaction):
-        """Provide the ability to rollback transaction state
-
-        Savepoints provide a way to:
-
-        - Save partial transaction work. For some data managers, this
-          could allow resources to be used more efficiently.
-
-        - Provide the ability to revert state to a point in a
-          transaction without aborting the entire transaction.  In
-          other words, savepoints support partial aborts.
-
-        Savepoints don't use two-phase commit. If there are errors in
-        setting or rolling back to savepoints, the application should
-        abort the containing transaction.  This is *not* the
-        responsibility of the data manager.
-
-        Savepoints are always associated with a transaction. Any work
-        done in a savepoint's transaction is tentative until the
-        transaction is committed using two-phase commit.
-
-        >>> dm = DataManager()
-        >>> dm.inc()
-        >>> t1 = '1'
-        >>> r = dm.savepoint(t1)
-        >>> dm.state, dm.delta
-        (0, 1)
-        >>> dm.inc()
-        >>> dm.state, dm.delta
-        (0, 2)
-        >>> r.rollback()
-        >>> dm.state, dm.delta
-        (0, 1)
-        >>> dm.prepare(t1)
-        >>> dm.commit(t1)
-        >>> dm.state, dm.delta
-        (1, 0)
-
-        Savepoints must have the same transaction:
-
-        >>> r1 = dm.savepoint(t1)
-        >>> dm.state, dm.delta
-        (1, 0)
-        >>> dm.inc()
-        >>> dm.state, dm.delta
-        (1, 1)
-        >>> t2 = '2'
-        >>> r2 = dm.savepoint(t2)
-        Traceback (most recent call last):
-        ...
-        TypeError: ('Transaction missmatch', '2', '1')
-
-        >>> r2 = dm.savepoint(t1)
-        >>> dm.inc()
-        >>> dm.state, dm.delta
-        (1, 2)
-
-        If we rollback to an earlier savepoint, we discard all work
-        done later:
-
-        >>> r1.rollback()
-        >>> dm.state, dm.delta
-        (1, 0)
-
-        and we can no longer rollback to the later savepoint:
-
-        >>> r2.rollback()
-        Traceback (most recent call last):
-        ...
-        TypeError: ('Attempt to roll back to invalid save point', 3, 2)
-
-        We can roll back to a savepoint as often as we like:
-
-        >>> r1.rollback()
-        >>> r1.rollback()
-        >>> r1.rollback()
-        >>> dm.state, dm.delta
-        (1, 0)
-
-        >>> dm.inc()
-        >>> dm.inc()
-        >>> dm.inc()
-        >>> dm.state, dm.delta
-        (1, 3)
-        >>> r1.rollback()
-        >>> dm.state, dm.delta
-        (1, 0)
-
-        But we can't rollback to a savepoint after it has been
-        committed:
-
-        >>> dm.prepare(t1)
-        >>> dm.commit(t1)
-
-        >>> r1.rollback()
-        Traceback (most recent call last):
-        ...
-        TypeError: Attempt to rollback stale rollback
-
-        """
-        if self.prepared:
-            raise TypeError("Can't get savepoint during two-phase commit")
-        self._checkTransaction(transaction)
-        self.transaction = transaction
-        self.sp += 1
-        return Rollback(self)
-
-class Rollback(object):
-
-    def __init__(self, dm):
-        self.dm = dm
-        self.sp = dm.sp
-        self.delta = dm.delta
-        self.transaction = dm.transaction
-
-    def rollback(self):
-        if self.transaction is not self.dm.transaction:
-            raise TypeError("Attempt to rollback stale rollback")
-        if self.dm.sp < self.sp:
-            raise TypeError("Attempt to roll back to invalid save point",
-                            self.sp, self.dm.sp)
-        self.dm.sp = self.sp
-        self.dm.delta = self.delta
-
-
-def test_suite():
-    from doctest import DocTestSuite
-    return DocTestSuite()
-
-if __name__ == '__main__':
-    unittest.main()

Deleted: transaction/trunk/transaction/tests/savepoint.txt
===================================================================
--- transaction/trunk/transaction/tests/savepoint.txt	2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/tests/savepoint.txt	2012-12-18 05:26:55 UTC (rev 128757)
@@ -1,290 +0,0 @@
-Savepoints
-==========
-
-Savepoints provide a way to save to disk intermediate work done during
-a transaction allowing:
-
-- partial transaction (subtransaction) rollback (abort)
-
-- state of saved objects to be freed, freeing on-line memory for other
-  uses
-
-Savepoints make it possible to write atomic subroutines that don't
-make top-level transaction commitments.
-
-
-Applications
-------------
-
-To demonstrate how savepoints work with transactions, we've provided a sample
-data manager implementation that provides savepoint support.  The primary
-purpose of this data manager is to provide code that can be read to understand
-how savepoints work.  The secondary purpose is to provide support for
-demonstrating the correct operation of savepoint support within the
-transaction system.  This data manager is very simple.  It provides flat
-storage of named immutable values, like strings and numbers.
-
-    >>> import transaction
-    >>> from transaction.tests import savepointsample
-    >>> dm = savepointsample.SampleSavepointDataManager()
-    >>> dm['name'] = 'bob'
-
-As with other data managers, we can commit changes:
-
-    >>> transaction.commit()
-    >>> dm['name']
-    'bob'
-
-and abort changes:
-
-    >>> dm['name'] = 'sally'
-    >>> dm['name']
-    'sally'
-    >>> transaction.abort()
-    >>> dm['name']
-    'bob'
-
-Now, let's look at an application that manages funds for people.  It allows
-deposits and debits to be entered for multiple people.  It accepts a sequence
-of entries and generates a sequence of status messages.  For each entry, it
-applies the change and then validates the user's account.  If the user's
-account is invalid, we roll back the change for that entry.  The success or
-failure of an entry is indicated in the output status.  First we'll initialize
-some accounts:
-
-    >>> dm['bob-balance'] = 0.0
-    >>> dm['bob-credit'] = 0.0
-    >>> dm['sally-balance'] = 0.0
-    >>> dm['sally-credit'] = 100.0
-    >>> transaction.commit()
-
-Now, we'll define a validation function to validate an account:
-
-    >>> def validate_account(name):
-    ...     if dm[name+'-balance'] + dm[name+'-credit'] < 0:
-    ...         raise ValueError('Overdrawn', name)
-
-And a function to apply entries.  If the function fails in some unexpected
-way, it rolls back all of its changes and prints the error:
-
-    >>> def apply_entries(entries):
-    ...     savepoint = transaction.savepoint()
-    ...     try:
-    ...         for name, amount in entries:
-    ...             entry_savepoint = transaction.savepoint()
-    ...             try:
-    ...                 dm[name+'-balance'] += amount
-    ...                 validate_account(name)
-    ...             except ValueError as error:
-    ...                 entry_savepoint.rollback()
-    ...                 print("%s %s" % ('Error', str(error)))
-    ...             else:
-    ...                 print("%s %s" % ('Updated', name))
-    ...     except Exception as error:
-    ...         savepoint.rollback()
-    ...         print("%s" % ('Unexpected exception'))
-
-Now let's try applying some entries:
-
-    >>> apply_entries([
-    ...     ('bob',   10.0),
-    ...     ('sally', 10.0),
-    ...     ('bob',   20.0),
-    ...     ('sally', 10.0),
-    ...     ('bob',   -100.0),
-    ...     ('sally', -100.0),
-    ...     ])
-    Updated bob
-    Updated sally
-    Updated bob
-    Updated sally
-    Error ('Overdrawn', 'bob')
-    Updated sally
-
-    >>> dm['bob-balance']
-    30.0
-
-    >>> dm['sally-balance']
-    -80.0
-
-If we provide entries that cause an unexpected error:
-
-    >>> apply_entries([
-    ...     ('bob',   10.0),
-    ...     ('sally', 10.0),
-    ...     ('bob',   '20.0'),
-    ...     ('sally', 10.0),
-    ...     ])
-    Updated bob
-    Updated sally
-    Unexpected exception
-
-Because the apply_entries used a savepoint for the entire function, it was
-able to rollback the partial changes without rolling back changes made in the
-previous call to ``apply_entries``:
-
-    >>> dm['bob-balance']
-    30.0
-
-    >>> dm['sally-balance']
-    -80.0
-
-If we now abort the outer transactions, the earlier changes will go
-away:
-
-    >>> transaction.abort()
-
-    >>> dm['bob-balance']
-    0.0
-
-    >>> dm['sally-balance']
-    0.0
-
-Savepoint invalidation
-----------------------
-
-A savepoint can be used any number of times:
-
-    >>> dm['bob-balance'] = 100.0
-    >>> dm['bob-balance']
-    100.0
-    >>> savepoint = transaction.savepoint()
-
-    >>> dm['bob-balance'] = 200.0
-    >>> dm['bob-balance']
-    200.0
-    >>> savepoint.rollback()
-    >>> dm['bob-balance']
-    100.0
-
-    >>> savepoint.rollback()  # redundant, but should be harmless
-    >>> dm['bob-balance']
-    100.0
-
-    >>> dm['bob-balance'] = 300.0
-    >>> dm['bob-balance']
-    300.0
-    >>> savepoint.rollback()
-    >>> dm['bob-balance']
-    100.0
-
-However, using a savepoint invalidates any savepoints that come after it:
-
-    >>> dm['bob-balance'] = 200.0
-    >>> dm['bob-balance']
-    200.0
-    >>> savepoint1 = transaction.savepoint()
-
-    >>> dm['bob-balance'] = 300.0
-    >>> dm['bob-balance']
-    300.0
-    >>> savepoint2 = transaction.savepoint()
-
-    >>> savepoint.rollback()
-    >>> dm['bob-balance']
-    100.0
-
-    >>> savepoint2.rollback() #doctest: +IGNORE_EXCEPTION_DETAIL
-    Traceback (most recent call last):
-    ...
-    InvalidSavepointRollbackError: invalidated by a later savepoint
-
-    >>> savepoint1.rollback() #doctest: +IGNORE_EXCEPTION_DETAIL
-    Traceback (most recent call last):
-    ...
-    InvalidSavepointRollbackError: invalidated by a later savepoint
-
-    >>> transaction.abort()
-
-
-Databases without savepoint support
------------------------------------
-
-Normally it's an error to use savepoints with databases that don't support
-savepoints:
-
-    >>> dm_no_sp = savepointsample.SampleDataManager()
-    >>> dm_no_sp['name'] = 'bob'
-    >>> transaction.commit()
-    >>> dm_no_sp['name'] = 'sally'
-    >>> transaction.savepoint() #doctest: +IGNORE_EXCEPTION_DETAIL
-    Traceback (most recent call last):
-    ...
-    TypeError: ('Savepoints unsupported', {'name': 'bob'})
-
-    >>> transaction.abort()
-
-However, a flag can be passed to the transaction savepoint method to indicate
-that databases without savepoint support should be tolerated until a savepoint
-is rolled back.  This allows transactions to proceed if there are no reasons
-to roll back:
-
-    >>> dm_no_sp['name'] = 'sally'
-    >>> savepoint = transaction.savepoint(1)
-    >>> dm_no_sp['name'] = 'sue'
-    >>> transaction.commit()
-    >>> dm_no_sp['name']
-    'sue'
-
-    >>> dm_no_sp['name'] = 'sam'
-    >>> savepoint = transaction.savepoint(1)
-    >>> savepoint.rollback() #doctest: +IGNORE_EXCEPTION_DETAIL
-    Traceback (most recent call last): 
-    ...
-    TypeError: ('Savepoints unsupported', {'name': 'sam'})
-
-
-Failures
---------
-
-If a failure occurs when creating or rolling back a savepoint, the transaction
-state will be uncertain and the transaction will become uncommitable.  From
-that point on, most transaction operations, including commit, will fail until
-the transaction is aborted.
-
-In the previous example, we got an error when we tried to rollback the
-savepoint.  If we try to commit the transaction, the commit will fail:
-
-    >>> transaction.commit() #doctest: +IGNORE_EXCEPTION_DETAIL
-    Traceback (most recent call last):
-    ...
-    TransactionFailedError: An operation previously failed, with traceback:
-    ...
-    TypeError: ('Savepoints unsupported', {'name': 'sam'})
-    <BLANKLINE>
-
-We have to abort it to make any progress:
-
-    >>> transaction.abort()
-
-Similarly, in our earlier example, where we tried to take a savepoint with a
-data manager that didn't support savepoints:
-
-    >>> dm_no_sp['name'] = 'sally'
-    >>> dm['name'] = 'sally'
-    >>> savepoint = transaction.savepoint() # doctest: +IGNORE_EXCEPTION_DETAIL
-    Traceback (most recent call last):
-    ...
-    TypeError: ('Savepoints unsupported', {'name': 'sue'})
-
-    >>> transaction.commit() # doctest: +IGNORE_EXCEPTION_DETAIL
-    Traceback (most recent call last):
-    ...
-    TransactionFailedError: An operation previously failed, with traceback:
-    ...
-    TypeError: ('Savepoints unsupported', {'name': 'sue'})
-    <BLANKLINE>
-
-    >>> transaction.abort()
-
-After clearing the transaction with an abort, we can get on with new
-transactions:
-
-    >>> dm_no_sp['name'] = 'sally'
-    >>> dm['name'] = 'sally'
-    >>> transaction.commit()
-    >>> dm_no_sp['name']
-    'sally'
-    >>> dm['name']
-    'sally'
-

Modified: transaction/trunk/transaction/tests/savepointsample.py
===================================================================
--- transaction/trunk/transaction/tests/savepointsample.py	2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/tests/savepointsample.py	2012-12-18 05:26:55 UTC (rev 128757)
@@ -16,7 +16,7 @@
 Sample data manager implementation that illustrates how to implement
 savepoints.
 
-See savepoint.txt in the transaction package.
+Used by savepoint.rst in the Sphinx docs.
 """
 
 from zope.interface import implementer

Deleted: transaction/trunk/transaction/tests/test_SampleDataManager.py
===================================================================
--- transaction/trunk/transaction/tests/test_SampleDataManager.py	2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/tests/test_SampleDataManager.py	2012-12-18 05:26:55 UTC (rev 128757)
@@ -1,413 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2004 Zope Foundation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE.
-#
-##############################################################################
-"""Sample objects for use in tests
-"""
-from doctest import DocTestSuite
-
-class DataManager(object):
-    """Sample data manager
-
-       This class provides a trivial data-manager implementation and doc
-       strings to illustrate the the protocol and to provide a tool for
-       writing tests.
-
-       Our sample data manager has state that is updated through an inc
-       method and through transaction operations.
-
-       When we create a sample data manager:
-
-       >>> dm = DataManager()
-
-       It has two bits of state, state:
-
-       >>> dm.state
-       0
-
-       and delta:
-
-       >>> dm.delta
-       0
-
-       Both of which are initialized to 0.  state is meant to model
-       committed state, while delta represents tentative changes within a
-       transaction.  We change the state by calling inc:
-
-       >>> dm.inc()
-
-       which updates delta:
-
-       >>> dm.delta
-       1
-
-       but state isn't changed until we commit the transaction:
-
-       >>> dm.state
-       0
-
-       To commit the changes, we use 2-phase commit. We execute the first
-       stage by calling prepare.  We need to pass a transation. Our
-       sample data managers don't really use the transactions for much,
-       so we'll be lazy and use strings for transactions:
-
-       >>> t1 = '1'
-       >>> dm.prepare(t1)
-
-       The sample data manager updates the state when we call prepare:
-
-       >>> dm.state
-       1
-       >>> dm.delta
-       1
-
-       This is mainly so we can detect some affect of calling the methods.
-
-       Now if we call commit:
-
-       >>> dm.commit(t1)
-
-       Our changes are"permanent".  The state reflects the changes and the
-       delta has been reset to 0.
-
-       >>> dm.state
-       1
-       >>> dm.delta
-       0
-       """
-
-    def __init__(self):
-        self.state = 0
-        self.sp = 0
-        self.transaction = None
-        self.delta = 0
-        self.prepared = False
-
-    def inc(self, n=1):
-        self.delta += n
-
-    def prepare(self, transaction):
-        """Prepare to commit data
-
-        >>> dm = DataManager()
-        >>> dm.inc()
-        >>> t1 = '1'
-        >>> dm.prepare(t1)
-        >>> dm.commit(t1)
-        >>> dm.state
-        1
-        >>> dm.inc()
-        >>> t2 = '2'
-        >>> dm.prepare(t2)
-        >>> dm.abort(t2)
-        >>> dm.state
-        1
-
-        It is en error to call prepare more than once without an intervening
-        commit or abort:
-
-        >>> dm.prepare(t1)
-
-        >>> dm.prepare(t1)
-        Traceback (most recent call last):
-        ...
-        TypeError: Already prepared
-
-        >>> dm.prepare(t2)
-        Traceback (most recent call last):
-        ...
-        TypeError: Already prepared
-
-        >>> dm.abort(t1)
-
-        If there was a preceeding savepoint, the transaction must match:
-
-        >>> rollback = dm.savepoint(t1)
-        >>> dm.prepare(t2)
-        Traceback (most recent call last):
-        ,,,
-        TypeError: ('Transaction missmatch', '2', '1')
-
-        >>> dm.prepare(t1)
-
-        """
-        if self.prepared:
-            raise TypeError('Already prepared')
-        self._checkTransaction(transaction)
-        self.prepared = True
-        self.transaction = transaction
-        self.state += self.delta
-
-    def _checkTransaction(self, transaction):
-        if (transaction is not self.transaction
-            and self.transaction is not None):
-            raise TypeError("Transaction missmatch",
-                            transaction, self.transaction)
-
-    def abort(self, transaction):
-        """Abort a transaction
-
-        The abort method can be called before two-phase commit to
-        throw away work done in the transaction:
-
-        >>> dm = DataManager()
-        >>> dm.inc()
-        >>> dm.state, dm.delta
-        (0, 1)
-        >>> t1 = '1'
-        >>> dm.abort(t1)
-        >>> dm.state, dm.delta
-        (0, 0)
-
-        The abort method also throws away work done in savepoints:
-
-        >>> dm.inc()
-        >>> r = dm.savepoint(t1)
-        >>> dm.inc()
-        >>> r = dm.savepoint(t1)
-        >>> dm.state, dm.delta
-        (0, 2)
-        >>> dm.abort(t1)
-        >>> dm.state, dm.delta
-        (0, 0)
-
-        If savepoints are used, abort must be passed the same
-        transaction:
-
-        >>> dm.inc()
-        >>> r = dm.savepoint(t1)
-        >>> t2 = '2'
-        >>> dm.abort(t2)
-        Traceback (most recent call last):
-        ...
-        TypeError: ('Transaction missmatch', '2', '1')
-
-        >>> dm.abort(t1)
-
-        The abort method is also used to abort a two-phase commit:
-
-        >>> dm.inc()
-        >>> dm.state, dm.delta
-        (0, 1)
-        >>> dm.prepare(t1)
-        >>> dm.state, dm.delta
-        (1, 1)
-        >>> dm.abort(t1)
-        >>> dm.state, dm.delta
-        (0, 0)
-
-        Of course, the transactions passed to prepare and abort must
-        match:
-
-        >>> dm.prepare(t1)
-        >>> dm.abort(t2)
-        Traceback (most recent call last):
-        ...
-        TypeError: ('Transaction missmatch', '2', '1')
-
-        >>> dm.abort(t1)
-
-
-        """
-        self._checkTransaction(transaction)
-        if self.transaction is not None:
-            self.transaction = None
-
-        if self.prepared:
-            self.state -= self.delta
-            self.prepared = False
-
-        self.delta = 0
-
-    def commit(self, transaction):
-        """Complete two-phase commit
-
-        >>> dm = DataManager()
-        >>> dm.state
-        0
-        >>> dm.inc()
-
-        We start two-phase commit by calling prepare:
-
-        >>> t1 = '1'
-        >>> dm.prepare(t1)
-
-        We complete it by calling commit:
-
-        >>> dm.commit(t1)
-        >>> dm.state
-        1
-
-        It is an error ro call commit without calling prepare first:
-
-        >>> dm.inc()
-        >>> t2 = '2'
-        >>> dm.commit(t2)
-        Traceback (most recent call last):
-        ...
-        TypeError: Not prepared to commit
-
-        >>> dm.prepare(t2)
-        >>> dm.commit(t2)
-
-        If course, the transactions given to prepare and commit must
-        be the same:
-
-        >>> dm.inc()
-        >>> t3 = '3'
-        >>> dm.prepare(t3)
-        >>> dm.commit(t2)
-        Traceback (most recent call last):
-        ...
-        TypeError: ('Transaction missmatch', '2', '3')
-
-        """
-        if not self.prepared:
-            raise TypeError('Not prepared to commit')
-        self._checkTransaction(transaction)
-        self.delta = 0
-        self.transaction = None
-        self.prepared = False
-
-    def savepoint(self, transaction):
-        """Provide the ability to rollback transaction state
-
-        Savepoints provide a way to:
-
-        - Save partial transaction work. For some data managers, this
-          could allow resources to be used more efficiently.
-
-        - Provide the ability to revert state to a point in a
-          transaction without aborting the entire transaction.  In
-          other words, savepoints support partial aborts.
-
-        Savepoints don't use two-phase commit. If there are errors in
-        setting or rolling back to savepoints, the application should
-        abort the containing transaction.  This is *not* the
-        responsibility of the data manager.
-
-        Savepoints are always associated with a transaction. Any work
-        done in a savepoint's transaction is tentative until the
-        transaction is committed using two-phase commit.
-
-        >>> dm = DataManager()
-        >>> dm.inc()
-        >>> t1 = '1'
-        >>> r = dm.savepoint(t1)
-        >>> dm.state, dm.delta
-        (0, 1)
-        >>> dm.inc()
-        >>> dm.state, dm.delta
-        (0, 2)
-        >>> r.rollback()
-        >>> dm.state, dm.delta
-        (0, 1)
-        >>> dm.prepare(t1)
-        >>> dm.commit(t1)
-        >>> dm.state, dm.delta
-        (1, 0)
-
-        Savepoints must have the same transaction:
-
-        >>> r1 = dm.savepoint(t1)
-        >>> dm.state, dm.delta
-        (1, 0)
-        >>> dm.inc()
-        >>> dm.state, dm.delta
-        (1, 1)
-        >>> t2 = '2'
-        >>> r2 = dm.savepoint(t2)
-        Traceback (most recent call last):
-        ...
-        TypeError: ('Transaction missmatch', '2', '1')
-
-        >>> r2 = dm.savepoint(t1)
-        >>> dm.inc()
-        >>> dm.state, dm.delta
-        (1, 2)
-
-        If we rollback to an earlier savepoint, we discard all work
-        done later:
-
-        >>> r1.rollback()
-        >>> dm.state, dm.delta
-        (1, 0)
-
-        and we can no longer rollback to the later savepoint:
-
-        >>> r2.rollback()
-        Traceback (most recent call last):
-        ...
-        TypeError: ('Attempt to roll back to invalid save point', 3, 2)
-
-        We can roll back to a savepoint as often as we like:
-
-        >>> r1.rollback()
-        >>> r1.rollback()
-        >>> r1.rollback()
-        >>> dm.state, dm.delta
-        (1, 0)
-
-        >>> dm.inc()
-        >>> dm.inc()
-        >>> dm.inc()
-        >>> dm.state, dm.delta
-        (1, 3)
-        >>> r1.rollback()
-        >>> dm.state, dm.delta
-        (1, 0)
-
-        But we can't rollback to a savepoint after it has been
-        committed:
-
-        >>> dm.prepare(t1)
-        >>> dm.commit(t1)
-
-        >>> r1.rollback()
-        Traceback (most recent call last):
-        ...
-        TypeError: Attempt to rollback stale rollback
-
-        """
-        if self.prepared:
-            raise TypeError("Can't get savepoint during two-phase commit")
-        self._checkTransaction(transaction)
-        self.transaction = transaction
-        self.sp += 1
-        return Rollback(self)
-
-class Rollback(object):
-
-    def __init__(self, dm):
-        self.dm = dm
-        self.sp = dm.sp
-        self.delta = dm.delta
-        self.transaction = dm.transaction
-
-    def rollback(self):
-        if self.transaction is not self.dm.transaction:
-            raise TypeError("Attempt to rollback stale rollback")
-        if self.dm.sp < self.sp:
-            raise TypeError("Attempt to roll back to invalid save point",
-                            self.sp, self.dm.sp)
-        self.dm.sp = self.sp
-        self.dm.delta = self.delta
-
-
-def test_suite():
-    return DocTestSuite()
-
-# additional_tests is for setuptools "setup.py test" support
-additional_tests = test_suite
-
-if __name__ == '__main__':
-    unittest.main()

Deleted: transaction/trunk/transaction/tests/test_SampleResourceManager.py
===================================================================
--- transaction/trunk/transaction/tests/test_SampleResourceManager.py	2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/tests/test_SampleResourceManager.py	2012-12-18 05:26:55 UTC (rev 128757)
@@ -1,438 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2004 Zope Foundation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE.
-#
-##############################################################################
-"""Sample objects for use in tests
-
-$Id$
-"""
-
-class ResourceManager(object):
-    """Sample resource manager.
-
-       This class provides a trivial resource-manager implementation and doc
-       strings to illustrate the protocol and to provide a tool for writing
-       tests.
-
-       Our sample resource manager has state that is updated through an inc
-       method and through transaction operations.
-
-       When we create a sample resource manager:
-
-       >>> rm = ResourceManager()
-
-       It has two pieces state, state and delta, both initialized to 0:
-
-       >>> rm.state
-       0
-       >>> rm.delta
-       0
-
-       state is meant to model committed state, while delta represents
-       tentative changes within a transaction.  We change the state by
-       calling inc:
-
-       >>> rm.inc()
-
-       which updates delta:
-
-       >>> rm.delta
-       1
-
-       but state isn't changed until we commit the transaction:
-
-       >>> rm.state
-       0
-
-       To commit the changes, we use 2-phase commit.  We execute the first
-       stage by calling prepare.  We need to pass a transation. Our
-       sample resource managers don't really use the transactions for much,
-       so we'll be lazy and use strings for transactions.  The sample
-       resource manager updates the state when we call tpc_vote:
-
-
-       >>> t1 = '1'
-       >>> rm.tpc_begin(t1)
-       >>> rm.state, rm.delta
-       (0, 1)
-
-       >>> rm.tpc_vote(t1)
-       >>> rm.state, rm.delta
-       (1, 1)
-
-       Now if we call tpc_finish:
-
-       >>> rm.tpc_finish(t1)
-
-       Our changes are "permanent".  The state reflects the changes and the
-       delta has been reset to 0.
-
-       >>> rm.state, rm.delta
-       (1, 0)
-       """
-
-    def __init__(self):
-        self.state = 0
-        self.sp = 0
-        self.transaction = None
-        self.delta = 0
-        self.txn_state = None
-
-    def _check_state(self, *ok_states):
-        if self.txn_state not in ok_states:
-            raise ValueError("txn in state %r but expected one of %r" %
-                             (self.txn_state, ok_states))
-
-    def _checkTransaction(self, transaction):
-        if (transaction is not self.transaction
-            and self.transaction is not None):
-            raise TypeError("Transaction missmatch",
-                            transaction, self.transaction)
-
-    def inc(self, n=1):
-        self.delta += n
-
-    def tpc_begin(self, transaction):
-        """Prepare to commit data.
-
-        >>> rm = ResourceManager()
-        >>> rm.inc()
-        >>> t1 = '1'
-        >>> rm.tpc_begin(t1)
-        >>> rm.tpc_vote(t1)
-        >>> rm.tpc_finish(t1)
-        >>> rm.state
-        1
-        >>> rm.inc()
-        >>> t2 = '2'
-        >>> rm.tpc_begin(t2)
-        >>> rm.tpc_vote(t2)
-        >>> rm.tpc_abort(t2)
-        >>> rm.state
-        1
-
-        It is an error to call tpc_begin more than once without completing
-        two-phase commit:
-
-        >>> rm.tpc_begin(t1)
-
-        >>> rm.tpc_begin(t1)
-        Traceback (most recent call last):
-        ...
-        ValueError: txn in state 'tpc_begin' but expected one of (None,)
-        >>> rm.tpc_abort(t1)
-
-        If there was a preceeding savepoint, the transaction must match:
-
-        >>> rollback = rm.savepoint(t1)
-        >>> rm.tpc_begin(t2)
-        Traceback (most recent call last):
-        ,,,
-        TypeError: ('Transaction missmatch', '2', '1')
-
-        >>> rm.tpc_begin(t1)
-
-        """
-        self._checkTransaction(transaction)
-        self._check_state(None)
-        self.transaction = transaction
-        self.txn_state = 'tpc_begin'
-
-    def tpc_vote(self, transaction):
-        """Verify that a data manager can commit the transaction.
-
-        This is the last chance for a data manager to vote 'no'.  A
-        data manager votes 'no' by raising an exception.
-
-        transaction is the ITransaction instance associated with the
-        transaction being committed.
-        """
-        self._checkTransaction(transaction)
-        self._check_state('tpc_begin')
-        self.state += self.delta
-        self.txn_state = 'tpc_vote'
-
-    def tpc_finish(self, transaction):
-        """Complete two-phase commit
-
-        >>> rm = ResourceManager()
-        >>> rm.state
-        0
-        >>> rm.inc()
-
-        We start two-phase commit by calling prepare:
-
-        >>> t1 = '1'
-        >>> rm.tpc_begin(t1)
-        >>> rm.tpc_vote(t1)
-
-        We complete it by calling tpc_finish:
-
-        >>> rm.tpc_finish(t1)
-        >>> rm.state
-        1
-
-        It is an error ro call tpc_finish without calling tpc_vote:
-
-        >>> rm.inc()
-        >>> t2 = '2'
-        >>> rm.tpc_begin(t2)
-        >>> rm.tpc_finish(t2)
-        Traceback (most recent call last):
-        ...
-        ValueError: txn in state 'tpc_begin' but expected one of ('tpc_vote',)
-
-        >>> rm.tpc_abort(t2)  # clean slate
-
-        >>> rm.tpc_begin(t2)
-        >>> rm.tpc_vote(t2)
-        >>> rm.tpc_finish(t2)
-
-        Of course, the transactions given to tpc_begin and tpc_finish must
-        be the same:
-
-        >>> rm.inc()
-        >>> t3 = '3'
-        >>> rm.tpc_begin(t3)
-        >>> rm.tpc_vote(t3)
-        >>> rm.tpc_finish(t2)
-        Traceback (most recent call last):
-        ...
-        TypeError: ('Transaction missmatch', '2', '3')
-        """
-        self._checkTransaction(transaction)
-        self._check_state('tpc_vote')
-        self.delta = 0
-        self.transaction = None
-        self.prepared = False
-        self.txn_state = None
-
-    def tpc_abort(self, transaction):
-        """Abort a transaction
-
-        The abort method can be called before two-phase commit to
-        throw away work done in the transaction:
-
-        >>> rm = ResourceManager()
-        >>> rm.inc()
-        >>> rm.state, rm.delta
-        (0, 1)
-        >>> t1 = '1'
-        >>> rm.tpc_abort(t1)
-        >>> rm.state, rm.delta
-        (0, 0)
-
-        The abort method also throws away work done in savepoints:
-
-        >>> rm.inc()
-        >>> r = rm.savepoint(t1)
-        >>> rm.inc()
-        >>> r = rm.savepoint(t1)
-        >>> rm.state, rm.delta
-        (0, 2)
-        >>> rm.tpc_abort(t1)
-        >>> rm.state, rm.delta
-        (0, 0)
-
-        If savepoints are used, abort must be passed the same
-        transaction:
-
-        >>> rm.inc()
-        >>> r = rm.savepoint(t1)
-        >>> t2 = '2'
-        >>> rm.tpc_abort(t2)
-        Traceback (most recent call last):
-        ...
-        TypeError: ('Transaction missmatch', '2', '1')
-
-        >>> rm.tpc_abort(t1)
-
-        The abort method is also used to abort a two-phase commit:
-
-        >>> rm.inc()
-        >>> rm.state, rm.delta
-        (0, 1)
-        >>> rm.tpc_begin(t1)
-        >>> rm.state, rm.delta
-        (0, 1)
-        >>> rm.tpc_vote(t1)
-        >>> rm.state, rm.delta
-        (1, 1)
-        >>> rm.tpc_abort(t1)
-        >>> rm.state, rm.delta
-        (0, 0)
-
-        Of course, the transactions passed to prepare and abort must
-        match:
-
-        >>> rm.tpc_begin(t1)
-        >>> rm.tpc_abort(t2)
-        Traceback (most recent call last):
-        ...
-        TypeError: ('Transaction missmatch', '2', '1')
-
-        >>> rm.tpc_abort(t1)
-
-        This should never fail.
-        """
-
-        self._checkTransaction(transaction)
-        if self.transaction is not None:
-            self.transaction = None
-
-        if self.txn_state == 'tpc_vote':
-            self.state -= self.delta
-
-        self.txn_state = None
-        self.delta = 0
-
-    def savepoint(self, transaction):
-        """Provide the ability to rollback transaction state
-
-        Savepoints provide a way to:
-
-        - Save partial transaction work. For some resource managers, this
-          could allow resources to be used more efficiently.
-
-        - Provide the ability to revert state to a point in a
-          transaction without aborting the entire transaction.  In
-          other words, savepoints support partial aborts.
-
-        Savepoints don't use two-phase commit. If there are errors in
-        setting or rolling back to savepoints, the application should
-        abort the containing transaction.  This is *not* the
-        responsibility of the resource manager.
-
-        Savepoints are always associated with a transaction. Any work
-        done in a savepoint's transaction is tentative until the
-        transaction is committed using two-phase commit.
-
-        >>> rm = ResourceManager()
-        >>> rm.inc()
-        >>> t1 = '1'
-        >>> r = rm.savepoint(t1)
-        >>> rm.state, rm.delta
-        (0, 1)
-        >>> rm.inc()
-        >>> rm.state, rm.delta
-        (0, 2)
-        >>> r.rollback()
-        >>> rm.state, rm.delta
-        (0, 1)
-        >>> rm.tpc_begin(t1)
-        >>> rm.tpc_vote(t1)
-        >>> rm.tpc_finish(t1)
-        >>> rm.state, rm.delta
-        (1, 0)
-
-        Savepoints must have the same transaction:
-
-        >>> r1 = rm.savepoint(t1)
-        >>> rm.state, rm.delta
-        (1, 0)
-        >>> rm.inc()
-        >>> rm.state, rm.delta
-        (1, 1)
-        >>> t2 = '2'
-        >>> r2 = rm.savepoint(t2)
-        Traceback (most recent call last):
-        ...
-        TypeError: ('Transaction missmatch', '2', '1')
-
-        >>> r2 = rm.savepoint(t1)
-        >>> rm.inc()
-        >>> rm.state, rm.delta
-        (1, 2)
-
-        If we rollback to an earlier savepoint, we discard all work
-        done later:
-
-        >>> r1.rollback()
-        >>> rm.state, rm.delta
-        (1, 0)
-
-        and we can no longer rollback to the later savepoint:
-
-        >>> r2.rollback()
-        Traceback (most recent call last):
-        ...
-        TypeError: ('Attempt to roll back to invalid save point', 3, 2)
-
-        We can roll back to a savepoint as often as we like:
-
-        >>> r1.rollback()
-        >>> r1.rollback()
-        >>> r1.rollback()
-        >>> rm.state, rm.delta
-        (1, 0)
-
-        >>> rm.inc()
-        >>> rm.inc()
-        >>> rm.inc()
-        >>> rm.state, rm.delta
-        (1, 3)
-        >>> r1.rollback()
-        >>> rm.state, rm.delta
-        (1, 0)
-
-        But we can't rollback to a savepoint after it has been
-        committed:
-
-        >>> rm.tpc_begin(t1)
-        >>> rm.tpc_vote(t1)
-        >>> rm.tpc_finish(t1)
-
-        >>> r1.rollback()
-        Traceback (most recent call last):
-        ...
-        TypeError: Attempt to rollback stale rollback
-
-        """
-        if self.txn_state is not None:
-            raise TypeError("Can't get savepoint during two-phase commit")
-        self._checkTransaction(transaction)
-        self.transaction = transaction
-        self.sp += 1
-        return SavePoint(self)
-
-    def discard(self, transaction):
-        pass
-
-class SavePoint(object):
-
-    def __init__(self, rm):
-        self.rm = rm
-        self.sp = rm.sp
-        self.delta = rm.delta
-        self.transaction = rm.transaction
-
-    def rollback(self):
-        if self.transaction is not self.rm.transaction:
-            raise TypeError("Attempt to rollback stale rollback")
-        if self.rm.sp < self.sp:
-            raise TypeError("Attempt to roll back to invalid save point",
-                            self.sp, self.rm.sp)
-        self.rm.sp = self.sp
-        self.rm.delta = self.delta
-
-    def discard(self):
-        pass
-
-def test_suite():
-    from doctest import DocTestSuite
-    return DocTestSuite()
-
-# additional_tests is for setuptools "setup.py test" support
-additional_tests = test_suite
-
-if __name__ == '__main__':
-    unittest.main()

Copied: transaction/trunk/transaction/tests/test__manager.py (from rev 128756, transaction/branches/sphinx/transaction/tests/test__manager.py)
===================================================================
--- transaction/trunk/transaction/tests/test__manager.py	                        (rev 0)
+++ transaction/trunk/transaction/tests/test__manager.py	2012-12-18 05:26:55 UTC (rev 128757)
@@ -0,0 +1,608 @@
+##############################################################################
+#
+# Copyright (c) 2012 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+import unittest
+
+
+class TransactionManagerTests(unittest.TestCase):
+
+    def _getTargetClass(self):
+        from transaction import TransactionManager
+        return TransactionManager
+
+    def _makeOne(self):
+        return self._getTargetClass()()
+
+    def _makePopulated(self):
+        mgr = self._makeOne()
+        sub1 = DataObject(mgr)
+        sub2 = DataObject(mgr)
+        sub3 = DataObject(mgr)
+        nosub1 = DataObject(mgr, nost=1)
+        return mgr, sub1, sub2, sub3, nosub1
+
+    def test_ctor(self):
+        tm = self._makeOne()
+        self.assertTrue(tm._txn is None)
+        self.assertEqual(len(tm._synchs), 0)
+
+    def test_begin_wo_existing_txn_wo_synchs(self):
+        from transaction._transaction import Transaction
+        tm = self._makeOne()
+        tm.begin()
+        self.assertTrue(isinstance(tm._txn, Transaction))
+
+    def test_begin_wo_existing_txn_w_synchs(self):
+        from transaction._transaction import Transaction
+        tm = self._makeOne()
+        synch = DummySynch()
+        tm.registerSynch(synch)
+        tm.begin()
+        self.assertTrue(isinstance(tm._txn, Transaction))
+        self.assertTrue(tm._txn in synch._txns)
+
+    def test_begin_w_existing_txn(self):
+        class Existing(object):
+            _aborted = False
+            def abort(self):
+                self._aborted = True
+        tm = self._makeOne()
+        tm._txn = txn = Existing()
+        tm.begin()
+        self.assertFalse(tm._txn is txn)
+        self.assertTrue(txn._aborted)
+
+    def test_get_wo_existing_txn(self):
+        from transaction._transaction import Transaction
+        tm = self._makeOne()
+        txn = tm.get()
+        self.assertTrue(isinstance(txn, Transaction))
+
+    def test_get_w_existing_txn(self):
+        class Existing(object):
+            _aborted = False
+            def abort(self):
+                self._aborted = True
+        tm = self._makeOne()
+        tm._txn = txn = Existing()
+        self.assertTrue(tm.get() is txn)
+
+    def test_free_w_other_txn(self):
+        from transaction._transaction import Transaction
+        tm = self._makeOne()
+        txn = Transaction()
+        tm.begin()
+        self.assertRaises(ValueError, tm.free, txn)
+
+    def test_free_w_existing_txn(self):
+        class Existing(object):
+            _aborted = False
+            def abort(self):
+                self._aborted = True
+        tm = self._makeOne()
+        tm._txn = txn = Existing()
+        tm.free(txn)
+        self.assertTrue(tm._txn is None)
+
+    def test_registerSynch(self):
+        tm = self._makeOne()
+        synch = DummySynch()
+        tm.registerSynch(synch)
+        self.assertEqual(len(tm._synchs), 1)
+        self.assertTrue(synch in tm._synchs)
+
+    def test_unregisterSynch(self):
+        tm = self._makeOne()
+        synch1 = DummySynch()
+        synch2 = DummySynch()
+        tm.registerSynch(synch1)
+        tm.registerSynch(synch2)
+        tm.unregisterSynch(synch1)
+        self.assertEqual(len(tm._synchs), 1)
+        self.assertFalse(synch1 in tm._synchs)
+        self.assertTrue(synch2 in tm._synchs)
+
+    def test_isDoomed_wo_existing_txn(self):
+        tm = self._makeOne()
+        self.assertFalse(tm.isDoomed())
+        tm._txn.doom()
+        self.assertTrue(tm.isDoomed())
+
+    def test_isDoomed_w_existing_txn(self):
+        class Existing(object):
+            _doomed = False
+            def isDoomed(self):
+                return self._doomed
+        tm = self._makeOne()
+        tm._txn = txn = Existing()
+        self.assertFalse(tm.isDoomed())
+        txn._doomed = True
+        self.assertTrue(tm.isDoomed())
+
+    def test_doom(self):
+        tm = self._makeOne()
+        txn = tm.get()
+        self.assertFalse(txn.isDoomed())
+        tm.doom()
+        self.assertTrue(txn.isDoomed())
+        self.assertTrue(tm.isDoomed())
+
+    def test_commit_w_existing_txn(self):
+        class Existing(object):
+            _committed = False
+            def commit(self):
+                self._committed = True
+        tm = self._makeOne()
+        tm._txn = txn = Existing()
+        tm.commit()
+        self.assertTrue(txn._committed)
+
+    def test_abort_w_existing_txn(self):
+        class Existing(object):
+            _aborted = False
+            def abort(self):
+                self._aborted = True
+        tm = self._makeOne()
+        tm._txn = txn = Existing()
+        tm.abort()
+        self.assertTrue(txn._aborted)
+
+    def test_as_context_manager_wo_error(self):
+        class _Test(object):
+            _committed = False
+            _aborted = False
+            def commit(self):
+                self._committed = True
+            def abort(self):
+                self._aborted = True
+        tm = self._makeOne()
+        with tm:
+            tm._txn = txn = _Test()
+        self.assertTrue(txn._committed)
+        self.assertFalse(txn._aborted)
+
+    def test_as_context_manager_w_error(self):
+        class _Test(object):
+            _committed = False
+            _aborted = False
+            def commit(self):
+                self._committed = True
+            def abort(self):
+                self._aborted = True
+        tm = self._makeOne()
+        try:
+            with tm:
+                tm._txn = txn = _Test()
+                1/0
+        except ZeroDivisionError: 
+            pass
+        self.assertFalse(txn._committed)
+        self.assertTrue(txn._aborted)
+
+    def test_savepoint_default(self):
+        class _Test(object):
+            _sp = None
+            def savepoint(self, optimistic):
+                self._sp = optimistic
+        tm = self._makeOne()
+        tm._txn = txn = _Test()
+        tm.savepoint()
+        self.assertFalse(txn._sp)
+
+    def test_savepoint_explicit(self):
+        class _Test(object):
+            _sp = None
+            def savepoint(self, optimistic):
+                self._sp = optimistic
+        tm = self._makeOne()
+        tm._txn = txn = _Test()
+        tm.savepoint(True)
+        self.assertTrue(txn._sp)
+
+    def test_attempts_w_invalid_count(self):
+        tm = self._makeOne()
+        self.assertRaises(ValueError, list, tm.attempts(0))
+        self.assertRaises(ValueError, list, tm.attempts(-1))
+        self.assertRaises(ValueError, list, tm.attempts(-10))
+
+    def test_attempts_w_valid_count(self):
+        tm = self._makeOne()
+        found = list(tm.attempts(1))
+        self.assertEqual(len(found), 1)
+        self.assertTrue(found[0] is tm)
+
+    def test_attempts_w_default_count(self):
+        from transaction._manager import Attempt
+        tm = self._makeOne()
+        found = list(tm.attempts())
+        self.assertEqual(len(found), 3)
+        for attempt in found[:-1]:
+            self.assertTrue(isinstance(attempt, Attempt))
+            self.assertTrue(attempt.manager is tm)
+        self.assertTrue(found[-1] is tm)
+
+    def test__retryable_w_transient_error(self):
+        from transaction.interfaces import TransientError
+        tm = self._makeOne()
+        self.assertTrue(tm._retryable(TransientError, object()))
+
+    def test__retryable_w_transient_subclass(self):
+        from transaction.interfaces import TransientError
+        class _Derived(TransientError):
+            pass
+        tm = self._makeOne()
+        self.assertTrue(tm._retryable(_Derived, object()))
+
+    def test__retryable_w_normal_exception_no_resources(self):
+        tm = self._makeOne()
+        self.assertFalse(tm._retryable(Exception, object()))
+
+    def test__retryable_w_normal_exception_w_resource_voting_yes(self):
+        class _Resource(object):
+            def should_retry(self, err):
+                return True
+        tm = self._makeOne()
+        tm.get()._resources.append(_Resource())
+        self.assertTrue(tm._retryable(Exception, object()))
+
+    # basic tests with two sub trans jars
+    # really we only need one, so tests for
+    # sub1 should identical to tests for sub2
+    def test_commit_normal(self):
+
+        mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
+        sub1.modify()
+        sub2.modify()
+
+        mgr.commit()
+
+        assert sub1._p_jar.ccommit_sub == 0
+        assert sub1._p_jar.ctpc_finish == 1
+
+    def test_abort_normal(self):
+
+        mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
+        sub1.modify()
+        sub2.modify()
+
+        mgr.abort()
+
+        assert sub2._p_jar.cabort == 1
+
+
+    # repeat adding in a nonsub trans jars
+
+    def test_commit_w_nonsub_jar(self):
+
+        mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
+        nosub1.modify()
+
+        mgr.commit()
+
+        assert nosub1._p_jar.ctpc_finish == 1
+
+    def test_abort_w_nonsub_jar(self):
+
+        mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
+        nosub1.modify()
+
+        mgr.abort()
+
+        assert nosub1._p_jar.ctpc_finish == 0
+        assert nosub1._p_jar.cabort == 1
+
+
+    ### Failure Mode Tests
+    #
+    # ok now we do some more interesting
+    # tests that check the implementations
+    # error handling by throwing errors from
+    # various jar methods
+    ###
+
+    # first the recoverable errors
+
+    def test_abort_w_broken_jar(self):
+        from transaction import _transaction
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
+            sub1._p_jar = BasicJar(errors='abort')
+            nosub1.modify()
+            sub1.modify(nojar=1)
+            sub2.modify()
+            try:
+                mgr.abort()
+            except TestTxnException:
+                pass
+
+        assert nosub1._p_jar.cabort == 1
+        assert sub2._p_jar.cabort == 1
+
+    def test_commit_w_broken_jar_commit(self):
+        from transaction import _transaction
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
+            sub1._p_jar = BasicJar(errors='commit')
+            nosub1.modify()
+            sub1.modify(nojar=1)
+            try:
+                mgr.commit()
+            except TestTxnException:
+                pass
+
+        assert nosub1._p_jar.ctpc_finish == 0
+        assert nosub1._p_jar.ccommit == 1
+        assert nosub1._p_jar.ctpc_abort == 1
+
+    def test_commit_w_broken_jar_tpc_vote(self):
+        from transaction import _transaction
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
+            sub1._p_jar = BasicJar(errors='tpc_vote')
+            nosub1.modify()
+            sub1.modify(nojar=1)
+            try:
+                mgr.commit()
+            except TestTxnException:
+                pass
+
+        assert nosub1._p_jar.ctpc_finish == 0
+        assert nosub1._p_jar.ccommit == 1
+        assert nosub1._p_jar.ctpc_abort == 1
+        assert sub1._p_jar.ctpc_abort == 1
+
+    def test_commit_w_broken_jar_tpc_begin(self):
+        # ok this test reveals a bug in the TM.py
+        # as the nosub tpc_abort there is ignored.
+
+        # nosub calling method tpc_begin
+        # nosub calling method commit
+        # sub calling method tpc_begin
+        # sub calling method abort
+        # sub calling method tpc_abort
+        # nosub calling method tpc_abort
+        from transaction import _transaction
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
+            sub1._p_jar = BasicJar(errors='tpc_begin')
+            nosub1.modify()
+            sub1.modify(nojar=1)
+            try:
+                mgr.commit()
+            except TestTxnException:
+                pass
+
+        assert nosub1._p_jar.ctpc_abort == 1
+        assert sub1._p_jar.ctpc_abort == 1
+
+    def test_commit_w_broken_jar_tpc_abort_tpc_vote(self):
+        from transaction import _transaction
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
+            sub1._p_jar = BasicJar(errors=('tpc_abort', 'tpc_vote'))
+            nosub1.modify()
+            sub1.modify(nojar=1)
+            try:
+                mgr.commit()
+            except TestTxnException:
+                pass
+
+        assert nosub1._p_jar.ctpc_abort == 1
+
+
+class AttemptTests(unittest.TestCase):
+
+    def _makeOne(self, manager):
+        from transaction._manager import Attempt
+        return Attempt(manager)
+
+    def test___enter__(self):
+        manager = DummyManager()
+        inst = self._makeOne(manager)
+        inst.__enter__()
+        self.assertTrue(manager.entered)
+
+    def test___exit__no_exc_no_commit_exception(self):
+        manager = DummyManager()
+        inst = self._makeOne(manager)
+        result = inst.__exit__(None, None, None)
+        self.assertFalse(result)
+        self.assertTrue(manager.committed)
+
+    def test___exit__no_exc_nonretryable_commit_exception(self):
+        manager = DummyManager(raise_on_commit=ValueError)
+        inst = self._makeOne(manager)
+        self.assertRaises(ValueError, inst.__exit__, None, None, None)
+        self.assertTrue(manager.committed)
+        self.assertTrue(manager.aborted)
+
+    def test___exit__no_exc_abort_exception_after_nonretryable_commit_exc(self):
+        manager = DummyManager(raise_on_abort=ValueError, 
+                               raise_on_commit=KeyError)
+        inst = self._makeOne(manager)
+        self.assertRaises(ValueError, inst.__exit__, None, None, None)
+        self.assertTrue(manager.committed)
+        self.assertTrue(manager.aborted)
+        
+    def test___exit__no_exc_retryable_commit_exception(self):
+        from transaction.interfaces import TransientError
+        manager = DummyManager(raise_on_commit=TransientError)
+        inst = self._makeOne(manager)
+        result = inst.__exit__(None, None, None)
+        self.assertTrue(result)
+        self.assertTrue(manager.committed)
+        self.assertTrue(manager.aborted)
+
+    def test___exit__with_exception_value_retryable(self):
+        from transaction.interfaces import TransientError
+        manager = DummyManager()
+        inst = self._makeOne(manager)
+        result = inst.__exit__(TransientError, TransientError(), None)
+        self.assertTrue(result)
+        self.assertFalse(manager.committed)
+        self.assertTrue(manager.aborted)
+
+    def test___exit__with_exception_value_nonretryable(self):
+        manager = DummyManager()
+        inst = self._makeOne(manager)
+        self.assertRaises(KeyError, inst.__exit__, KeyError, KeyError(), None)
+        self.assertFalse(manager.committed)
+        self.assertTrue(manager.aborted)
+        
+
+class DummyManager(object):
+    entered = False
+    committed = False
+    aborted = False
+    
+    def __init__(self, raise_on_commit=None, raise_on_abort=None):
+        self.raise_on_commit = raise_on_commit
+        self.raise_on_abort = raise_on_abort
+
+    def _retryable(self, t, v):
+        from transaction._manager import TransientError
+        return issubclass(t, TransientError)
+        
+    def __enter__(self):
+        self.entered = True
+
+    def abort(self):
+        self.aborted = True
+        if self.raise_on_abort:
+            raise self.raise_on_abort
+        
+    def commit(self):
+        self.committed = True
+        if self.raise_on_commit:
+            raise self.raise_on_commit
+
+
+class DataObject:
+
+    def __init__(self, transaction_manager, nost=0):
+        self.transaction_manager = transaction_manager
+        self.nost = nost
+        self._p_jar = None
+
+    def modify(self, nojar=0, tracing=0):
+        if not nojar:
+            if self.nost:
+                self._p_jar = BasicJar(tracing=tracing)
+            else:
+                self._p_jar = BasicJar(tracing=tracing)
+        self.transaction_manager.get().join(self._p_jar)
+
+
+class TestTxnException(Exception):
+    pass
+
+
+class BasicJar:
+
+    def __init__(self, errors=(), tracing=0):
+        if not isinstance(errors, tuple):
+            errors = errors,
+        self.errors = errors
+        self.tracing = tracing
+        self.cabort = 0
+        self.ccommit = 0
+        self.ctpc_begin = 0
+        self.ctpc_abort = 0
+        self.ctpc_vote = 0
+        self.ctpc_finish = 0
+        self.cabort_sub = 0
+        self.ccommit_sub = 0
+
+    def __repr__(self):
+        return "<%s %X %s>" % (self.__class__.__name__,
+                               positive_id(self),
+                               self.errors)
+
+    def sortKey(self):
+        # All these jars use the same sort key, and Python's list.sort()
+        # is stable.  These two
+        return self.__class__.__name__
+
+    def check(self, method):
+        if self.tracing:
+            print('%s calling method %s'%(str(self.tracing),method))
+
+        if method in self.errors:
+            raise TestTxnException("error %s" % method)
+
+    ## basic jar txn interface
+
+    def abort(self, *args):
+        self.check('abort')
+        self.cabort += 1
+
+    def commit(self, *args):
+        self.check('commit')
+        self.ccommit += 1
+
+    def tpc_begin(self, txn, sub=0):
+        self.check('tpc_begin')
+        self.ctpc_begin += 1
+
+    def tpc_vote(self, *args):
+        self.check('tpc_vote')
+        self.ctpc_vote += 1
+
+    def tpc_abort(self, *args):
+        self.check('tpc_abort')
+        self.ctpc_abort += 1
+
+    def tpc_finish(self, *args):
+        self.check('tpc_finish')
+        self.ctpc_finish += 1
+
+
+class DummySynch(object):
+    def __init__(self):
+        self._txns = set()
+    def newTransaction(self, txn):
+        self._txns.add(txn)
+
+
+def positive_id(obj):
+    """Return id(obj) as a non-negative integer."""
+    import struct
+    _ADDRESS_MASK = 256 ** struct.calcsize('P')
+
+    result = id(obj)
+    if result < 0:
+        result += _ADDRESS_MASK
+        assert result > 0
+    return result
+
+
+def test_suite():
+    return unittest.TestSuite((
+        unittest.makeSuite(TransactionManagerTests),
+        unittest.makeSuite(AttemptTests),
+    ))

Copied: transaction/trunk/transaction/tests/test__transaction.py (from rev 128495, transaction/trunk/transaction/tests/test_transaction.py)
===================================================================
--- transaction/trunk/transaction/tests/test__transaction.py	                        (rev 0)
+++ transaction/trunk/transaction/tests/test__transaction.py	2012-12-18 05:26:55 UTC (rev 128757)
@@ -0,0 +1,1439 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002, 2005 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+"""Test transaction behavior for variety of cases.
+
+I wrote these unittests to investigate some odd transaction
+behavior when doing unittests of integrating non sub transaction
+aware objects, and to insure proper txn behavior. these
+tests test the transaction system independent of the rest of the
+zodb.
+
+you can see the method calls to a jar by passing the
+keyword arg tracing to the modify method of a dataobject.
+the value of the arg is a prefix used for tracing print calls
+to that objects jar.
+
+the number of times a jar method was called can be inspected
+by looking at an attribute of the jar that is the method
+name prefixed with a c (count/check).
+
+i've included some tracing examples for tests that i thought
+were illuminating as doc strings below.
+
+TODO
+
+    add in tests for objects which are modified multiple times,
+    for example an object that gets modified in multiple sub txns.
+"""
+import unittest
+
+
+class TransactionTests(unittest.TestCase):
+
+    def _getTargetClass(self):
+        from transaction._transaction import Transaction
+        return Transaction
+
+    def _makeOne(self, synchronizers=None, manager=None):
+        return self._getTargetClass()(synchronizers, manager)
+
+    def test_ctor_defaults(self):
+        from transaction.weakset import WeakSet
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        from transaction import _transaction
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            txn = self._makeOne()
+        self.assertTrue(isinstance(txn._synchronizers, WeakSet))
+        self.assertEqual(len(txn._synchronizers), 0)
+        self.assertTrue(txn._manager is None)
+        self.assertEqual(txn.user, "")
+        self.assertEqual(txn.description, "")
+        self.assertTrue(txn._savepoint2index is None)
+        self.assertEqual(txn._savepoint_index, 0)
+        self.assertEqual(txn._resources, [])
+        self.assertEqual(txn._adapters, {})
+        self.assertEqual(txn._voted, {})
+        self.assertEqual(txn._extension, {})
+        self.assertTrue(txn.log is logger)
+        self.assertEqual(len(logger._log), 1)
+        self.assertEqual(logger._log[0][0], 'debug')
+        self.assertEqual(logger._log[0][1], 'new transaction')
+        self.assertTrue(txn._failure_traceback is None)
+        self.assertEqual(txn._before_commit, [])
+        self.assertEqual(txn._after_commit, [])
+
+    def test_ctor_w_syncs(self):
+        from transaction.weakset import WeakSet
+        synchs = WeakSet()
+        txn = self._makeOne(synchronizers=synchs)
+        self.assertTrue(txn._synchronizers is synchs)
+
+    def test_isDoomed(self):
+        from transaction._transaction import Status
+        txn = self._makeOne()
+        self.assertFalse(txn.isDoomed())
+        txn.status = Status.DOOMED
+        self.assertTrue(txn.isDoomed())
+
+    def test_doom_active(self):
+        from transaction._transaction import Status
+        txn = self._makeOne()
+        txn.doom()
+        self.assertTrue(txn.isDoomed())
+        self.assertEqual(txn.status, Status.DOOMED)
+
+    def test_doom_invalid(self):
+        from transaction._transaction import Status
+        txn = self._makeOne()
+        for status in Status.COMMITTING, Status.COMMITTED, Status.COMMITFAILED:
+            txn.status = status
+            self.assertRaises(ValueError, txn.doom)
+
+    def test_doom_already_doomed(self):
+        from transaction._transaction import Status
+        txn = self._makeOne()
+        txn.status = Status.DOOMED
+        self.assertTrue(txn.isDoomed())
+        self.assertEqual(txn.status, Status.DOOMED)
+
+    def test__prior_operation_failed(self):
+        from transaction.interfaces import TransactionFailedError
+        from transaction.tests.common import assertRaisesEx
+        class _Traceback(object):
+            def getvalue(self):
+                return 'TRACEBACK'
+        txn = self._makeOne()
+        txn._failure_traceback = _Traceback()
+        err = assertRaisesEx(TransactionFailedError,
+                             txn._prior_operation_failed)
+        self.assertTrue(str(err).startswith('An operation previously failed'))
+        self.assertTrue(str(err).endswith( "with traceback:\n\nTRACEBACK"))
+
+    def test_join_COMMITFAILED(self):
+        from transaction.interfaces import TransactionFailedError
+        from transaction._transaction import Status
+        class _Traceback(object):
+            def getvalue(self):
+                return 'TRACEBACK'
+        txn = self._makeOne()
+        txn.status = Status.COMMITFAILED
+        txn._failure_traceback = _Traceback()
+        self.assertRaises(TransactionFailedError, txn.join, object())
+
+    def test_join_COMMITTING(self):
+        from transaction._transaction import Status
+        txn = self._makeOne()
+        txn.status = Status.COMMITTING
+        self.assertRaises(ValueError, txn.join, object())
+
+    def test_join_COMMITTED(self):
+        from transaction._transaction import Status
+        txn = self._makeOne()
+        txn.status = Status.COMMITTED
+        self.assertRaises(ValueError, txn.join, object())
+
+    def test_join_DOOMED_non_preparing_wo_sp2index(self):
+        from transaction._transaction import Status
+        txn = self._makeOne()
+        txn.status = Status.DOOMED
+        resource = object()
+        txn.join(resource)
+        self.assertEqual(txn._resources, [resource])
+
+    def test_join_ACTIVE_w_preparing_w_sp2index(self):
+        from transaction._transaction import AbortSavepoint
+        from transaction._transaction import DataManagerAdapter
+        class _TSP(object):
+            def __init__(self):
+                self._savepoints = []
+        class _DM(object):
+            def prepare(self):
+                pass
+        txn = self._makeOne()
+        tsp = _TSP()
+        txn._savepoint2index = {tsp: object()}
+        dm = _DM
+        txn.join(dm)
+        self.assertEqual(len(txn._resources), 1)
+        dma = txn._resources[0]
+        self.assertTrue(isinstance(dma, DataManagerAdapter))
+        self.assertTrue(txn._resources[0]._datamanager is dm)
+        self.assertEqual(len(tsp._savepoints), 1)
+        self.assertTrue(isinstance(tsp._savepoints[0], AbortSavepoint))
+        self.assertTrue(tsp._savepoints[0].datamanager is dma)
+        self.assertTrue(tsp._savepoints[0].transaction is txn)
+
+    def test__unjoin_miss(self):
+        txn = self._makeOne()
+        txn._unjoin(object()) #no raise
+
+    def test__unjoin_hit(self):
+        txn = self._makeOne()
+        resource = object()
+        txn._resources.append(resource)
+        txn._unjoin(resource)
+        self.assertEqual(txn._resources, [])
+
+    def test_savepoint_COMMITFAILED(self):
+        from transaction.interfaces import TransactionFailedError
+        from transaction._transaction import Status
+        class _Traceback(object):
+            def getvalue(self):
+                return 'TRACEBACK'
+        txn = self._makeOne()
+        txn.status = Status.COMMITFAILED
+        txn._failure_traceback = _Traceback()
+        self.assertRaises(TransactionFailedError, txn.savepoint)
+
+    def test_savepoint_empty(self):
+        from weakref import WeakKeyDictionary
+        from transaction import _transaction
+        from transaction._transaction import Savepoint
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            txn = self._makeOne()
+        sp = txn.savepoint()
+        self.assertTrue(isinstance(sp, Savepoint))
+        self.assertTrue(sp.transaction is txn)
+        self.assertEqual(sp._savepoints, [])
+        self.assertEqual(txn._savepoint_index, 1)
+        self.assertTrue(isinstance(txn._savepoint2index, WeakKeyDictionary))
+        self.assertEqual(txn._savepoint2index[sp], 1)
+
+    def test_savepoint_non_optimistc_resource_wo_support(self):
+        from transaction import _transaction
+        from transaction._transaction import Status
+        from transaction._compat import StringIO
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            txn = self._makeOne()
+        logger._clear()
+        resource = object()
+        txn._resources.append(resource)
+        self.assertRaises(TypeError, txn.savepoint)
+        self.assertEqual(txn.status,  Status.COMMITFAILED)
+        self.assertTrue(isinstance(txn._failure_traceback, StringIO))
+        self.assertTrue('TypeError' in txn._failure_traceback.getvalue())
+        self.assertEqual(len(logger._log), 2)
+        self.assertEqual(logger._log[0][0], 'error')
+        self.assertTrue(logger._log[0][1].startswith('Error in abort'))
+        self.assertEqual(logger._log[1][0], 'error')
+        self.assertTrue(logger._log[1][1].startswith('Error in tpc_abort'))
+
+    def test__remove_and_invalidate_after_miss(self):
+        from weakref import WeakKeyDictionary
+        txn = self._makeOne()
+        txn._savepoint2index = WeakKeyDictionary()
+        class _SP(object):
+            def __init__(self, txn):
+                self.transaction = txn
+        holdme = []
+        for i in range(10):
+            sp = _SP(txn)
+            holdme.append(sp) #prevent gc
+            txn._savepoint2index[sp] = i
+        self.assertEqual(len(txn._savepoint2index), 10)
+        self.assertRaises(KeyError, txn._remove_and_invalidate_after, _SP(txn))
+        self.assertEqual(len(txn._savepoint2index), 10)
+
+    def test__remove_and_invalidate_after_hit(self):
+        from weakref import WeakKeyDictionary
+        txn = self._makeOne()
+        txn._savepoint2index = WeakKeyDictionary()
+        class _SP(object):
+            def __init__(self, txn, index):
+                self.transaction = txn
+                self._index = index
+            def __lt__(self, other):
+                return self._index < other._index
+            def __repr__(self):
+                return '_SP: %d' % self._index
+        holdme = []
+        for i in range(10):
+            sp = _SP(txn, i)
+            holdme.append(sp) #prevent gc
+            txn._savepoint2index[sp] = i
+        self.assertEqual(len(txn._savepoint2index), 10)
+        txn._remove_and_invalidate_after(holdme[1])
+        self.assertEqual(sorted(txn._savepoint2index), sorted(holdme[:2]))
+
+    def test__invalidate_all_savepoints(self):
+        from weakref import WeakKeyDictionary
+        txn = self._makeOne()
+        txn._savepoint2index = WeakKeyDictionary()
+        class _SP(object):
+            def __init__(self, txn, index):
+                self.transaction = txn
+                self._index = index
+            def __repr__(self):
+                return '_SP: %d' % self._index
+        holdme = []
+        for i in range(10):
+            sp = _SP(txn, i)
+            holdme.append(sp) #prevent gc
+            txn._savepoint2index[sp] = i
+        self.assertEqual(len(txn._savepoint2index), 10)
+        txn._invalidate_all_savepoints()
+        self.assertEqual(list(txn._savepoint2index), [])
+
+    def test_register_wo_jar(self):
+        class _Dummy(object):
+            _p_jar = None
+        txn = self._makeOne()
+        self.assertRaises(ValueError, txn.register, _Dummy())
+
+    def test_register_w_jar(self):
+        class _Manager(object):
+            pass
+        mgr = _Manager()
+        class _Dummy(object):
+            _p_jar = mgr
+        txn = self._makeOne()
+        dummy = _Dummy()
+        txn.register(dummy)
+        resources = list(txn._resources)
+        self.assertEqual(len(resources), 1)
+        adapter = resources[0]
+        self.assertTrue(adapter.manager is mgr)
+        self.assertTrue(dummy in adapter.objects)
+        items = list(txn._adapters.items())
+        self.assertEqual(len(items), 1)
+        self.assertTrue(items[0][0] is mgr)
+        self.assertTrue(items[0][1] is adapter)
+
+    def test_register_w_jar_already_adapted(self):
+        class _Adapter(object):
+            def __init__(self):
+                self.objects = []
+        class _Manager(object):
+            pass
+        mgr = _Manager()
+        class _Dummy(object):
+            _p_jar = mgr
+        txn = self._makeOne()
+        txn._adapters[mgr] = adapter = _Adapter()
+        dummy = _Dummy()
+        txn.register(dummy)
+        self.assertTrue(dummy in adapter.objects)
+
+    def test_commit_DOOMED(self):
+        from transaction.interfaces import DoomedTransaction
+        from transaction._transaction import Status
+        txn = self._makeOne()
+        txn.status = Status.DOOMED
+        self.assertRaises(DoomedTransaction, txn.commit)
+
+    def test_commit_COMMITFAILED(self):
+        from transaction._transaction import Status
+        from transaction.interfaces import TransactionFailedError
+        class _Traceback(object):
+            def getvalue(self):
+                return 'TRACEBACK'
+        txn = self._makeOne()
+        txn.status = Status.COMMITFAILED
+        txn._failure_traceback = _Traceback()
+        self.assertRaises(TransactionFailedError, txn.commit)
+
+    def test_commit_wo_savepoints_wo_hooks_wo_synchronizers(self):
+        from transaction._transaction import Status
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        from transaction import _transaction
+        class _Mgr(object):
+            def __init__(self, txn):
+                self._txn = txn
+            def free(self, txn):
+                assert txn is self._txn
+                self._txn = None
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            txn = self._makeOne()
+            logger._clear()
+            mgr = txn._manager = _Mgr(txn)
+            txn.commit()
+        self.assertEqual(txn.status, Status.COMMITTED)
+        self.assertTrue(mgr._txn is None)
+        self.assertEqual(logger._log[0][0], 'debug')
+        self.assertEqual(logger._log[0][1], 'commit')
+
+    def test_commit_w_savepoints(self):
+        from weakref import WeakKeyDictionary
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        from transaction import _transaction
+        class _SP(object):
+            def __init__(self, txn, index):
+                self.transaction = txn
+                self._index = index
+            def __repr__(self):
+                return '_SP: %d' % self._index
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            txn = self._makeOne()
+            txn._savepoint2index = WeakKeyDictionary()
+            holdme = []
+            for i in range(10):
+                sp = _SP(txn, i)
+                holdme.append(sp) #prevent gc
+                txn._savepoint2index[sp] = i
+            logger._clear()
+            txn.commit()
+        self.assertEqual(list(txn._savepoint2index), [])
+
+    def test_commit_w_beforeCommitHooks(self):
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        from transaction import _transaction
+        _hooked1, _hooked2 = [], []
+        def _hook1(*args, **kw):
+            _hooked1.append((args, kw))
+        def _hook2(*args, **kw):
+            _hooked2.append((args, kw))
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            txn = self._makeOne()
+            txn._before_commit.append((_hook1, ('one',), {'uno': 1}))
+            txn._before_commit.append((_hook2, (), {}))
+            logger._clear()
+            txn.commit()
+        self.assertEqual(_hooked1, [(('one',), {'uno': 1})])
+        self.assertEqual(_hooked2, [((), {})])
+        self.assertEqual(txn._before_commit, [])
+
+    def test_commit_w_synchronizers(self):
+        from transaction.weakset import WeakSet
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        from transaction import _transaction
+        class _Synch(object):
+            _before = _after = False
+            def beforeCompletion(self, txn):
+                self._before = txn
+            def afterCompletion(self, txn):
+                self._after = txn
+        synchs = [_Synch(), _Synch(), _Synch()]
+        ws = WeakSet()
+        for synch in synchs:
+            ws.add(synch)
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            txn = self._makeOne(synchronizers=ws)
+            logger._clear()
+            txn.commit()
+        for synch in synchs:
+            self.assertTrue(synch._before is txn)
+            self.assertTrue(synch._after is txn)
+
+    def test_commit_w_afterCommitHooks(self):
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        from transaction import _transaction
+        _hooked1, _hooked2 = [], []
+        def _hook1(*args, **kw):
+            _hooked1.append((args, kw))
+        def _hook2(*args, **kw):
+            _hooked2.append((args, kw))
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            txn = self._makeOne()
+            txn._after_commit.append((_hook1, ('one',), {'uno': 1}))
+            txn._after_commit.append((_hook2, (), {}))
+            logger._clear()
+            txn.commit()
+        self.assertEqual(_hooked1, [((True, 'one',), {'uno': 1})])
+        self.assertEqual(_hooked2, [((True,), {})])
+        self.assertEqual(txn._after_commit, [])
+
+    def test_commit_error_w_afterCompleteHooks(self):
+        from transaction import _transaction
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        class BrokenResource(object):
+            def sortKey(self):
+                return 'zzz'
+            def tpc_begin(self, txn):
+                raise ValueError('test')
+        broken = BrokenResource()
+        resource = Resource('aaa')
+        _hooked1, _hooked2 = [], []
+        def _hook1(*args, **kw):
+            _hooked1.append((args, kw))
+        def _hook2(*args, **kw):
+            _hooked2.append((args, kw))
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            txn = self._makeOne()
+            txn._after_commit.append((_hook1, ('one',), {'uno': 1}))
+            txn._after_commit.append((_hook2, (), {}))
+            txn._resources.append(broken)
+            txn._resources.append(resource)
+            logger._clear()
+            self.assertRaises(ValueError, txn.commit)
+        self.assertEqual(_hooked1, [((False, 'one',), {'uno': 1})])
+        self.assertEqual(_hooked2, [((False,), {})])
+        self.assertEqual(txn._after_commit, [])
+        self.assertTrue(resource._b)
+        self.assertFalse(resource._c)
+        self.assertFalse(resource._v)
+        self.assertFalse(resource._f)
+        self.assertTrue(resource._a)
+        self.assertTrue(resource._x)
+
+    def test_commit_error_w_synchronizers(self):
+        from transaction.weakset import WeakSet
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        from transaction import _transaction
+        class _Synch(object):
+            _before = _after = False
+            def beforeCompletion(self, txn):
+                self._before = txn
+            def afterCompletion(self, txn):
+                self._after = txn
+        synchs = [_Synch(), _Synch(), _Synch()]
+        ws = WeakSet()
+        for synch in synchs:
+            ws.add(synch)
+        class BrokenResource(object):
+            def sortKey(self):
+                return 'zzz'
+            def tpc_begin(self, txn):
+                raise ValueError('test')
+        broken = BrokenResource()
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            txn = self._makeOne(synchronizers=ws)
+            logger._clear()
+            txn._resources.append(broken)
+            self.assertRaises(ValueError, txn.commit)
+        for synch in synchs:
+            self.assertTrue(synch._before is txn)
+            self.assertTrue(synch._after is txn) #called in _cleanup
+
+    def test_getBeforeCommitHooks_empty(self):
+        txn = self._makeOne()
+        self.assertEqual(list(txn.getBeforeCommitHooks()), [])
+
+    def test_addBeforeCommitHook(self):
+        def _hook(*args, **kw):
+            pass
+        txn = self._makeOne()
+        txn.addBeforeCommitHook(_hook, ('one',), dict(uno=1))
+        self.assertEqual(list(txn.getBeforeCommitHooks()),
+                         [(_hook, ('one',), {'uno': 1})])
+
+    def test_addBeforeCommitHook_w_kws(self):
+        def _hook(*args, **kw):
+            pass
+        txn = self._makeOne()
+        txn.addBeforeCommitHook(_hook, ('one',))
+        self.assertEqual(list(txn.getBeforeCommitHooks()),
+                         [(_hook, ('one',), {})])
+
+    def test_getAfterCommitHooks_empty(self):
+        txn = self._makeOne()
+        self.assertEqual(list(txn.getAfterCommitHooks()), [])
+
+    def test_addAfterCommitHook(self):
+        def _hook(*args, **kw):
+            pass
+        txn = self._makeOne()
+        txn.addAfterCommitHook(_hook, ('one',), dict(uno=1))
+        self.assertEqual(list(txn.getAfterCommitHooks()),
+                         [(_hook, ('one',), {'uno': 1})])
+
+    def test_addAfterCommitHook_wo_kws(self):
+        def _hook(*args, **kw):
+            pass
+        txn = self._makeOne()
+        txn.addAfterCommitHook(_hook, ('one',))
+        self.assertEqual(list(txn.getAfterCommitHooks()),
+                         [(_hook, ('one',), {})])
+
+    def test_callAfterCommitHook_w_error(self):
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        from transaction import _transaction
+        _hooked2 = []
+        def _hook1(*args, **kw):
+            raise ValueError()
+        def _hook2(*args, **kw):
+            _hooked2.append((args, kw))
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            txn = self._makeOne()
+        logger._clear()
+        txn.addAfterCommitHook(_hook1, ('one',))
+        txn.addAfterCommitHook(_hook2, ('two',), dict(dos=2))
+        txn._callAfterCommitHooks()
+        # second hook gets called even if first raises
+        self.assertEqual(_hooked2, [((True, 'two',), {'dos': 2})])
+        self.assertEqual(len(logger._log), 1)
+        self.assertEqual(logger._log[0][0], 'error')
+        self.assertTrue(logger._log[0][1].startswith(
+                            "Error in after commit hook"))
+
+    def test_callAfterCommitHook_w_abort(self):
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        from transaction import _transaction
+        _hooked2 = []
+        def _hook1(*args, **kw):
+            raise ValueError()
+        def _hook2(*args, **kw):
+            _hooked2.append((args, kw))
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            txn = self._makeOne()
+        logger._clear()
+        txn.addAfterCommitHook(_hook1, ('one',))
+        txn.addAfterCommitHook(_hook2, ('two',), dict(dos=2))
+        txn._callAfterCommitHooks()
+        self.assertEqual(logger._log[0][0], 'error')
+        self.assertTrue(logger._log[0][1].startswith(
+                            "Error in after commit hook"))
+
+    def test__commitResources_normal(self):
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        from transaction import _transaction
+        resources = [Resource('bbb'), Resource('aaa')]
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            txn = self._makeOne()
+        logger._clear()
+        txn._resources.extend(resources)
+        txn._commitResources()
+        self.assertEqual(len(txn._voted), 2)
+        for r in resources:
+            self.assertTrue(r._b and r._c and r._v and r._f)
+            self.assertFalse(r._a and r._x)
+            self.assertTrue(id(r) in txn._voted)
+        self.assertEqual(len(logger._log), 2)
+        self.assertEqual(logger._log[0][0], 'debug')
+        self.assertEqual(logger._log[0][1], 'commit Resource: aaa')
+        self.assertEqual(logger._log[1][0], 'debug')
+        self.assertEqual(logger._log[1][1], 'commit Resource: bbb')
+
+    def test__commitResources_error_in_tpc_begin(self):
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        from transaction import _transaction
+        resources = [Resource('bbb', 'tpc_begin'), Resource('aaa')]
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            txn = self._makeOne()
+        logger._clear()
+        txn._resources.extend(resources)
+        self.assertRaises(ValueError, txn._commitResources)
+        for r in resources:
+            if r._key == 'aaa':
+                self.assertTrue(r._b)
+            else:
+                self.assertFalse(r._b)
+            self.assertFalse(r._c and r._v and r._f)
+            self.assertTrue(r._a and r._x)
+        self.assertEqual(len(logger._log), 0)
+
+    def test__commitResources_error_in_commit(self):
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        from transaction import _transaction
+        resources = [Resource('bbb', 'commit'), Resource('aaa')]
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            txn = self._makeOne()
+        logger._clear()
+        txn._resources.extend(resources)
+        self.assertRaises(ValueError, txn._commitResources)
+        for r in resources:
+            self.assertTrue(r._b)
+            if r._key == 'aaa':
+                self.assertTrue(r._c)
+            else:
+                self.assertFalse(r._c)
+            self.assertFalse(r._v and r._f)
+            self.assertTrue(r._a and r._x)
+        self.assertEqual(len(logger._log), 1)
+        self.assertEqual(logger._log[0][0], 'debug')
+        self.assertEqual(logger._log[0][1], 'commit Resource: aaa')
+
+    def test__commitResources_error_in_tpc_vote(self):
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        from transaction import _transaction
+        resources = [Resource('bbb', 'tpc_vote'), Resource('aaa')]
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            txn = self._makeOne()
+        logger._clear()
+        txn._resources.extend(resources)
+        self.assertRaises(ValueError, txn._commitResources)
+        self.assertEqual(len(txn._voted), 1)
+        for r in resources:
+            self.assertTrue(r._b and r._c)
+            if r._key == 'aaa':
+                self.assertTrue(id(r) in txn._voted)
+                self.assertTrue(r._v)
+                self.assertFalse(r._f)
+                self.assertFalse(r._a)
+                self.assertTrue(r._x)
+            else:
+                self.assertFalse(id(r) in txn._voted)
+                self.assertFalse(r._v)
+                self.assertFalse(r._f)
+                self.assertTrue(r._a and r._x)
+        self.assertEqual(len(logger._log), 2)
+        self.assertEqual(logger._log[0][0], 'debug')
+        self.assertEqual(logger._log[0][1], 'commit Resource: aaa')
+        self.assertEqual(logger._log[1][0], 'debug')
+        self.assertEqual(logger._log[1][1], 'commit Resource: bbb')
+
+    def test__commitResources_error_in_tpc_finish(self):
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        from transaction import _transaction
+        resources = [Resource('bbb', 'tpc_finish'), Resource('aaa')]
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            txn = self._makeOne()
+        logger._clear()
+        txn._resources.extend(resources)
+        self.assertRaises(ValueError, txn._commitResources)
+        for r in resources:
+            self.assertTrue(r._b and r._c and r._v)
+            self.assertTrue(id(r) in txn._voted)
+            if r._key == 'aaa':
+                self.assertTrue(r._f)
+            else:
+                self.assertFalse(r._f)
+            self.assertFalse(r._a and r._x) #no cleanup if tpc_finish raises
+        self.assertEqual(len(logger._log), 3)
+        self.assertEqual(logger._log[0][0], 'debug')
+        self.assertEqual(logger._log[0][1], 'commit Resource: aaa')
+        self.assertEqual(logger._log[1][0], 'debug')
+        self.assertEqual(logger._log[1][1], 'commit Resource: bbb')
+        self.assertEqual(logger._log[2][0], 'critical')
+        self.assertTrue(logger._log[2][1].startswith(
+                        'A storage error occurred'))
+
+    def test_abort_wo_savepoints_wo_hooks_wo_synchronizers(self):
+        from transaction._transaction import Status
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        from transaction import _transaction
+        class _Mgr(object):
+            def __init__(self, txn):
+                self._txn = txn
+            def free(self, txn):
+                assert txn is self._txn
+                self._txn = None
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            txn = self._makeOne()
+            logger._clear()
+            mgr = txn._manager = _Mgr(txn)
+            txn.abort()
+        self.assertEqual(txn.status, Status.ACTIVE)
+        self.assertTrue(mgr._txn is None)
+        self.assertEqual(logger._log[0][0], 'debug')
+        self.assertEqual(logger._log[0][1], 'abort')
+
+    def test_abort_w_savepoints(self):
+        from weakref import WeakKeyDictionary
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        from transaction import _transaction
+        class _SP(object):
+            def __init__(self, txn, index):
+                self.transaction = txn
+                self._index = index
+            def __repr__(self):
+                return '_SP: %d' % self._index
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            txn = self._makeOne()
+            txn._savepoint2index = WeakKeyDictionary()
+            holdme = []
+            for i in range(10):
+                sp = _SP(txn, i)
+                holdme.append(sp) #prevent gc
+                txn._savepoint2index[sp] = i
+            logger._clear()
+            txn.abort()
+        self.assertEqual(list(txn._savepoint2index), [])
+
+    def test_abort_w_beforeCommitHooks(self):
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        from transaction import _transaction
+        _hooked1, _hooked2 = [], []
+        def _hook1(*args, **kw):
+            _hooked1.append((args, kw))
+        def _hook2(*args, **kw):
+            _hooked2.append((args, kw))
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            txn = self._makeOne()
+            txn._before_commit.append((_hook1, ('one',), {'uno': 1}))
+            txn._before_commit.append((_hook2, (), {}))
+            logger._clear()
+            txn.abort()
+        self.assertEqual(_hooked1, [])
+        self.assertEqual(_hooked2, [])
+        # Hooks are neither called nor cleared on abort
+        self.assertEqual(list(txn.getBeforeCommitHooks()),
+                         [(_hook1, ('one',), {'uno': 1}), (_hook2, (), {})])
+
+    def test_abort_w_synchronizers(self):
+        from transaction.weakset import WeakSet
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        from transaction import _transaction
+        class _Synch(object):
+            _before = _after = False
+            def beforeCompletion(self, txn):
+                self._before = txn
+            def afterCompletion(self, txn):
+                self._after = txn
+        synchs = [_Synch(), _Synch(), _Synch()]
+        ws = WeakSet()
+        for synch in synchs:
+            ws.add(synch)
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            txn = self._makeOne(synchronizers=ws)
+            logger._clear()
+            txn.abort()
+        for synch in synchs:
+            self.assertTrue(synch._before is txn)
+            self.assertTrue(synch._after is txn)
+
+    def test_abort_w_afterCommitHooks(self):
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        from transaction import _transaction
+        _hooked1, _hooked2 = [], []
+        def _hook1(*args, **kw):
+            _hooked1.append((args, kw))
+        def _hook2(*args, **kw):
+            _hooked2.append((args, kw))
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            txn = self._makeOne()
+            txn._after_commit.append((_hook1, ('one',), {'uno': 1}))
+            txn._after_commit.append((_hook2, (), {}))
+            logger._clear()
+            txn.abort()
+        # Hooks are neither called nor cleared on abort
+        self.assertEqual(_hooked1, [])
+        self.assertEqual(_hooked2, [])
+        self.assertEqual(list(txn.getAfterCommitHooks()),
+                         [(_hook1, ('one',), {'uno': 1}), (_hook2, (), {})])
+
+    def test_abort_error_w_afterCompleteHooks(self):
+        from transaction import _transaction
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        class BrokenResource(object):
+            def sortKey(self):
+                return 'zzz'
+            def abort(self, txn):
+                raise ValueError('test')
+        broken = BrokenResource()
+        resource = Resource('aaa')
+        _hooked1, _hooked2 = [], []
+        def _hook1(*args, **kw):
+            _hooked1.append((args, kw))
+        def _hook2(*args, **kw):
+            _hooked2.append((args, kw))
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            txn = self._makeOne()
+            txn._after_commit.append((_hook1, ('one',), {'uno': 1}))
+            txn._after_commit.append((_hook2, (), {}))
+            txn._resources.append(broken)
+            txn._resources.append(resource)
+            logger._clear()
+            self.assertRaises(ValueError, txn.abort)
+        # Hooks are neither called nor cleared on abort
+        self.assertEqual(_hooked1, [])
+        self.assertEqual(_hooked2, [])
+        self.assertEqual(list(txn.getAfterCommitHooks()),
+                         [(_hook1, ('one',), {'uno': 1}), (_hook2, (), {})])
+        self.assertTrue(resource._a)
+        self.assertFalse(resource._x)
+
+    def test_abort_error_w_synchronizers(self):
+        from transaction.weakset import WeakSet
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        from transaction import _transaction
+        class _Synch(object):
+            _before = _after = False
+            def beforeCompletion(self, txn):
+                self._before = txn
+            def afterCompletion(self, txn):
+                self._after = txn
+        synchs = [_Synch(), _Synch(), _Synch()]
+        ws = WeakSet()
+        for synch in synchs:
+            ws.add(synch)
+        class BrokenResource(object):
+            def sortKey(self):
+                return 'zzz'
+            def abort(self, txn):
+                raise ValueError('test')
+        broken = BrokenResource()
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            t = self._makeOne(synchronizers=ws)
+            logger._clear()
+            t._resources.append(broken)
+            self.assertRaises(ValueError, t.abort)
+        for synch in synchs:
+            self.assertTrue(synch._before is t)
+            self.assertTrue(synch._after is t) #called in _cleanup
+
+    def test_note(self):
+        txn = self._makeOne()
+        try:
+            txn.note('This is a note.')
+            self.assertEqual(txn.description, 'This is a note.')
+            txn.note('Another.')
+            self.assertEqual(txn.description, 'This is a note.\nAnother.')
+        finally:
+            txn.abort()
+
+    def test_setUser_default_path(self):
+        txn = self._makeOne()
+        txn.setUser('phreddy')
+        self.assertEqual(txn.user, '/ phreddy')
+
+    def test_setUser_explicit_path(self):
+        txn = self._makeOne()
+        txn.setUser('phreddy', '/bedrock')
+        self.assertEqual(txn.user, '/bedrock phreddy')
+
+    def test_setExtendedInfo_single(self):
+        txn = self._makeOne()
+        txn.setExtendedInfo('frob', 'qux')
+        self.assertEqual(txn._extension, {'frob': 'qux'})
+
+    def test_setExtendedInfo_multiple(self):
+        txn = self._makeOne()
+        txn.setExtendedInfo('frob', 'qux')
+        txn.setExtendedInfo('baz', 'spam')
+        txn.setExtendedInfo('frob', 'quxxxx')
+        self.assertEqual(txn._extension, {'frob': 'quxxxx', 'baz': 'spam'})
+
+
+class MultiObjectResourceAdapterTests(unittest.TestCase):
+
+    def _getTargetClass(self):
+        from transaction._transaction import MultiObjectResourceAdapter
+        return MultiObjectResourceAdapter
+
+    def _makeOne(self, jar):
+        return self._getTargetClass()(jar)
+
+    def _makeJar(self, key):
+        class _Resource(Resource):
+            def __init__(self, key):
+                super(_Resource, self).__init__(key)
+                self._c = []
+                self._a = []
+            def commit(self, obj, txn):
+                self._c.append((obj, txn))
+            def abort(self, obj, txn):
+                self._a.append((obj, txn))
+        return _Resource(key)
+
+    def _makeDummy(self, kind, name):
+        class _Dummy(object):
+            def __init__(self, kind, name):
+                self._kind = kind
+                self._name = name
+            def __repr__(self):
+                return '<%s: %s>' % (self._kind, self._name)
+        return _Dummy(kind, name)
+
+    def test_ctor(self):
+        jar = self._makeJar('aaa')
+        mora = self._makeOne(jar)
+        self.assertTrue(mora.manager is jar)
+        self.assertEqual(mora.objects, [])
+        self.assertEqual(mora.ncommitted, 0)
+
+    def test___repr__(self):
+        jar = self._makeJar('bbb')
+        mora = self._makeOne(jar)
+        self.assertEqual(repr(mora),
+                         '<MultiObjectResourceAdapter '
+                            'for Resource: bbb at %s>' % id(mora))
+
+    def test_sortKey(self):
+        jar = self._makeJar('ccc')
+        mora = self._makeOne(jar)
+        self.assertEqual(mora.sortKey(), 'ccc')
+
+    def test_tpc_begin(self):
+        jar = self._makeJar('ddd')
+        mora = self._makeOne(jar)
+        txn = object()
+        mora.tpc_begin(txn)
+        self.assertTrue(jar._b)
+
+    def test_commit(self):
+        jar = self._makeJar('eee')
+        objects = [self._makeDummy('obj', 'a'), self._makeDummy('obj', 'b')]
+        mora = self._makeOne(jar)
+        mora.objects.extend(objects)
+        txn = self._makeDummy('txn', 'c')
+        mora.commit(txn)
+        self.assertEqual(jar._c, [(objects[0], txn), (objects[1], txn)])
+
+    def test_tpc_vote(self):
+        jar = self._makeJar('fff')
+        mora = self._makeOne(jar)
+        txn = object()
+        mora.tpc_vote(txn)
+        self.assertTrue(jar._v)
+
+    def test_tpc_finish(self):
+        jar = self._makeJar('ggg')
+        mora = self._makeOne(jar)
+        txn = object()
+        mora.tpc_finish(txn)
+        self.assertTrue(jar._f)
+
+    def test_abort(self):
+        jar = self._makeJar('hhh')
+        objects = [self._makeDummy('obj', 'a'), self._makeDummy('obj', 'b')]
+        mora = self._makeOne(jar)
+        mora.objects.extend(objects)
+        txn = self._makeDummy('txn', 'c')
+        mora.abort(txn)
+        self.assertEqual(jar._a, [(objects[0], txn), (objects[1], txn)])
+
+    def test_abort_w_error(self):
+        from transaction.tests.common import DummyLogger
+        jar = self._makeJar('hhh')
+        objects = [self._makeDummy('obj', 'a'),
+                   self._makeDummy('obj', 'b'),
+                   self._makeDummy('obj', 'c'),
+                  ]
+        _old_abort = jar.abort
+        def _abort(obj, txn):
+            if obj._name == 'b':
+                raise ValueError()
+            _old_abort(obj, txn)
+        jar.abort = _abort
+        mora = self._makeOne(jar)
+        mora.objects.extend(objects)
+        txn = self._makeDummy('txn', 'c')
+        txn.log = log = DummyLogger()
+        self.assertRaises(ValueError, mora.abort, txn)
+        self.assertEqual(jar._a, [(objects[0], txn), (objects[2], txn)])
+
+    def test_tpc_abort(self):
+        jar = self._makeJar('iii')
+        mora = self._makeOne(jar)
+        txn = object()
+        mora.tpc_abort(txn)
+        self.assertTrue(jar._x)
+
+
+class Test_rm_key(unittest.TestCase):
+
+    def _callFUT(self, oid):
+        from transaction._transaction import rm_key
+        return rm_key(oid)
+
+    def test_miss(self):
+        self.assertTrue(self._callFUT(object()) is None)
+
+    def test_hit(self):
+        self.assertEqual(self._callFUT(Resource('zzz')), 'zzz')
+
+
+class Test_object_hint(unittest.TestCase):
+
+    def _callFUT(self, oid):
+        from transaction._transaction import object_hint
+        return object_hint(oid)
+
+    def test_miss(self):
+        class _Test(object):
+            pass
+        test = _Test()
+        self.assertEqual(self._callFUT(test), "_Test oid=None")
+
+    def test_hit(self):
+        class _Test(object):
+            pass
+        test = _Test()
+        test._p_oid = 'OID'
+        self.assertEqual(self._callFUT(test), "_Test oid='OID'")
+
+
+class Test_oid_repr(unittest.TestCase):
+
+    def _callFUT(self, oid):
+        from transaction._transaction import oid_repr
+        return oid_repr(oid)
+
+    def test_as_nonstring(self):
+        self.assertEqual(self._callFUT(123), '123')
+
+    def test_as_string_not_8_chars(self):
+        self.assertEqual(self._callFUT('a'), "'a'")
+
+    def test_as_string_z64(self):
+        s = '\0'*8
+        self.assertEqual(self._callFUT(s), '0x00')
+
+    def test_as_string_all_Fs(self):
+        s = '\1'*8
+        self.assertEqual(self._callFUT(s), '0x0101010101010101')
+
+
+class DataManagerAdapterTests(unittest.TestCase):
+
+    def _getTargetClass(self):
+        from transaction._transaction import DataManagerAdapter
+        return DataManagerAdapter
+
+    def _makeOne(self, jar):
+        return self._getTargetClass()(jar)
+
+    def _makeJar(self, key):
+        class _Resource(Resource):
+            _p = False
+            def prepare(self, txn):
+                self._p = True
+        return _Resource(key)
+
+    def _makeDummy(self, kind, name):
+        class _Dummy(object):
+            def __init__(self, kind, name):
+                self._kind = kind
+                self._name = name
+            def __repr__(self):
+                return '<%s: %s>' % (self._kind, self._name)
+        return _Dummy(kind, name)
+
+    def test_ctor(self):
+        jar = self._makeJar('aaa')
+        dma = self._makeOne(jar)
+        self.assertTrue(dma._datamanager is jar)
+
+    def test_commit(self):
+        jar = self._makeJar('bbb')
+        mora = self._makeOne(jar)
+        txn = self._makeDummy('txn', 'c')
+        mora.commit(txn)
+        self.assertFalse(jar._c) #no-op
+
+    def test_abort(self):
+        jar = self._makeJar('ccc')
+        mora = self._makeOne(jar)
+        txn = self._makeDummy('txn', 'c')
+        mora.abort(txn)
+        self.assertTrue(jar._a)
+
+    def test_tpc_begin(self):
+        jar = self._makeJar('ddd')
+        mora = self._makeOne(jar)
+        txn = object()
+        mora.tpc_begin(txn)
+        self.assertFalse(jar._b) #no-op
+
+    def test_tpc_abort(self):
+        jar = self._makeJar('eee')
+        mora = self._makeOne(jar)
+        txn = object()
+        mora.tpc_abort(txn)
+        self.assertFalse(jar._f)
+        self.assertTrue(jar._a)
+
+    def test_tpc_finish(self):
+        jar = self._makeJar('fff')
+        mora = self._makeOne(jar)
+        txn = object()
+        mora.tpc_finish(txn)
+        self.assertFalse(jar._f)
+        self.assertTrue(jar._c)
+
+    def test_tpc_vote(self):
+        jar = self._makeJar('ggg')
+        mora = self._makeOne(jar)
+        txn = object()
+        mora.tpc_vote(txn)
+        self.assertFalse(jar._v)
+        self.assertTrue(jar._p)
+
+    def test_sortKey(self):
+        jar = self._makeJar('hhh')
+        mora = self._makeOne(jar)
+        self.assertEqual(mora.sortKey(), 'hhh')
+
+
+class SavepointTests(unittest.TestCase):
+
+    def _getTargetClass(self):
+        from transaction._transaction import Savepoint
+        return Savepoint
+
+    def _makeOne(self, txn, optimistic, *resources):
+        return self._getTargetClass()(txn, optimistic, *resources)
+
+    def test_ctor_w_savepoint_oblivious_resource_non_optimistic(self):
+        txn = object()
+        resource = object()
+        self.assertRaises(TypeError, self._makeOne, txn, False, resource)
+
+    def test_ctor_w_savepoint_oblivious_resource_optimistic(self):
+        from transaction._transaction import NoRollbackSavepoint
+        txn = object()
+        resource = object()
+        sp = self._makeOne(txn, True, resource)
+        self.assertEqual(len(sp._savepoints), 1)
+        self.assertTrue(isinstance(sp._savepoints[0], NoRollbackSavepoint))
+        self.assertTrue(sp._savepoints[0].datamanager is resource)
+
+    def test_ctor_w_savepoint_aware_resources(self):
+        class _Aware(object):
+            def savepoint(self):
+                return self
+        txn = object()
+        one = _Aware()
+        another = _Aware()
+        sp = self._makeOne(txn, True, one, another)
+        self.assertEqual(len(sp._savepoints), 2)
+        self.assertTrue(isinstance(sp._savepoints[0], _Aware))
+        self.assertTrue(sp._savepoints[0] is one)
+        self.assertTrue(isinstance(sp._savepoints[1], _Aware))
+        self.assertTrue(sp._savepoints[1] is another)
+
+    def test_rollback_w_txn_None(self):
+        from transaction.interfaces import InvalidSavepointRollbackError
+        txn = None
+        class _Aware(object):
+            def savepoint(self):
+                return self
+        resource = _Aware()
+        sp = self._makeOne(txn, False, resource)
+        self.assertRaises(InvalidSavepointRollbackError, sp.rollback)
+
+    def test_rollback_w_sp_error(self):
+        class _TXN(object):
+            _sarce = False
+            _raia = None
+            def _saveAndRaiseCommitishError(self):
+                import sys
+                from transaction._compat import reraise
+                self._sarce = True
+                reraise(*sys.exc_info())
+            def _remove_and_invalidate_after(self, sp):
+                self._raia = sp
+        class _Broken(object):
+            def rollback(self):
+                raise ValueError()
+        _broken = _Broken()
+        class _GonnaRaise(object):
+            def savepoint(self):
+                return _broken
+        txn = _TXN()
+        resource = _GonnaRaise()
+        sp = self._makeOne(txn, False, resource)
+        self.assertRaises(ValueError, sp.rollback)
+        self.assertTrue(txn._raia is sp)
+        self.assertTrue(txn._sarce)
+
+
+class AbortSavepointTests(unittest.TestCase):
+
+    def _getTargetClass(self):
+        from transaction._transaction import AbortSavepoint
+        return AbortSavepoint
+
+    def _makeOne(self, datamanager, transaction):
+        return self._getTargetClass()(datamanager, transaction)
+
+    def test_ctor(self):
+        dm = object()
+        txn = object()
+        asp = self._makeOne(dm, txn)
+        self.assertTrue(asp.datamanager is dm)
+        self.assertTrue(asp.transaction is txn)
+
+    def test_rollback(self):
+        class _DM(object):
+            _aborted = None
+            def abort(self, txn):
+                self._aborted = txn
+        class _TXN(object):
+            _unjoined = None
+            def _unjoin(self, datamanager):
+                self._unjoin = datamanager
+        dm = _DM()
+        txn = _TXN()
+        asp = self._makeOne(dm, txn)
+        asp.rollback()
+        self.assertTrue(dm._aborted is txn)
+        self.assertTrue(txn._unjoin is dm)
+
+
+class NoRollbackSavepointTests(unittest.TestCase):
+
+    def _getTargetClass(self):
+        from transaction._transaction import NoRollbackSavepoint
+        return NoRollbackSavepoint
+
+    def _makeOne(self, datamanager):
+        return self._getTargetClass()(datamanager)
+
+    def test_ctor(self):
+        dm = object()
+        nrsp = self._makeOne(dm)
+        self.assertTrue(nrsp.datamanager is dm)
+
+    def test_rollback(self):
+        dm = object()
+        nrsp = self._makeOne(dm)
+        self.assertRaises(TypeError, nrsp.rollback)
+
+
+class MiscellaneousTests(unittest.TestCase):
+
+    def test_BBB_join(self):
+        # The join method is provided for "backward-compatability" with ZODB 4
+        # data managers.
+        from transaction import Transaction
+        from transaction.tests.examples import DataManager
+        from transaction._transaction import DataManagerAdapter
+        # The argument to join must be a zodb4 data manager,
+        # transaction.interfaces.IDataManager.
+        txn = Transaction()
+        dm = DataManager()
+        txn.join(dm)
+        # The end result is that a data manager adapter is one of the
+        # transaction's objects:
+        self.assertTrue(isinstance(txn._resources[0], DataManagerAdapter))
+        self.assertTrue(txn._resources[0]._datamanager is dm)
+
+    def test_bug239086(self):
+        # The original implementation of thread transaction manager made
+        # invalid assumptions about thread ids.
+        import threading
+        import transaction
+        import transaction.tests.savepointsample as SPS
+        dm = SPS.SampleSavepointDataManager()
+        self.assertEqual(list(dm.keys()), [])
+
+        class Sync:
+             def __init__(self, label):
+                 self.label = label
+                 self.log = []
+             def beforeCompletion(self, txn):
+                 self.log.append('%s %s' % (self.label, 'before'))
+             def afterCompletion(self, txn):
+                 self.log.append('%s %s' % (self.label, 'after'))
+             def newTransaction(self, txn):
+                 self.log.append('%s %s' % (self.label, 'new'))
+
+        def run_in_thread(f):
+            txn = threading.Thread(target=f)
+            txn.start()
+            txn.join()
+
+        sync = Sync(1)
+        @run_in_thread
+        def first():
+            transaction.manager.registerSynch(sync)
+            transaction.manager.begin()
+            dm['a'] = 1
+        self.assertEqual(sync.log, ['1 new'])
+
+        @run_in_thread
+        def second():
+            transaction.abort() # should do nothing.
+        self.assertEqual(sync.log, ['1 new'])
+        self.assertEqual(list(dm.keys()), ['a'])
+
+        dm = SPS.SampleSavepointDataManager()
+        self.assertEqual(list(dm.keys()), [])
+
+        @run_in_thread
+        def third():
+            dm['a'] = 1
+        self.assertEqual(sync.log, ['1 new'])
+
+        transaction.abort() # should do nothing
+        self.assertEqual(list(dm.keys()), ['a'])
+
+class Resource(object):
+    _b = _c = _v = _f = _a = _x = False
+    def __init__(self, key, error=None):
+        self._key = key
+        self._error = error
+    def __repr__(self):
+        return 'Resource: %s' % self._key
+    def sortKey(self):
+        return self._key
+    def tpc_begin(self, txn):
+        if self._error == 'tpc_begin':
+            raise ValueError()
+        self._b = True
+    def commit(self, txn):
+        if self._error == 'commit':
+            raise ValueError()
+        self._c = True
+    def tpc_vote(self, txn):
+        if self._error == 'tpc_vote':
+            raise ValueError()
+        self._v = True
+    def tpc_finish(self, txn):
+        if self._error == 'tpc_finish':
+            raise ValueError()
+        self._f = True
+    def abort(self, txn):
+        if self._error == 'abort':
+            raise ValueError()
+        self._a = True
+    def tpc_abort(self, txn):
+        if self._error == 'tpc_abort':
+            raise ValueError()
+        self._x = True
+
+def test_suite():
+    return unittest.TestSuite((
+        unittest.makeSuite(TransactionTests),
+        unittest.makeSuite(MultiObjectResourceAdapterTests),
+        unittest.makeSuite(Test_rm_key),
+        unittest.makeSuite(Test_object_hint),
+        unittest.makeSuite(Test_oid_repr),
+        unittest.makeSuite(DataManagerAdapterTests),
+        unittest.makeSuite(SavepointTests),
+        unittest.makeSuite(AbortSavepointTests),
+        unittest.makeSuite(NoRollbackSavepointTests),
+        unittest.makeSuite(MiscellaneousTests),
+        ))

Deleted: transaction/trunk/transaction/tests/test_attempt.py
===================================================================
--- transaction/trunk/transaction/tests/test_attempt.py	2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/tests/test_attempt.py	2012-12-18 05:26:55 UTC (rev 128757)
@@ -1,85 +0,0 @@
-import unittest
-
-class TestAttempt(unittest.TestCase):
-    def _makeOne(self, manager):
-        from transaction._manager import Attempt
-        return Attempt(manager)
-
-    def test___enter__(self):
-        manager = DummyManager()
-        inst = self._makeOne(manager)
-        inst.__enter__()
-        self.assertTrue(manager.entered)
-
-    def test___exit__no_exc_no_commit_exception(self):
-        manager = DummyManager()
-        inst = self._makeOne(manager)
-        result = inst.__exit__(None, None, None)
-        self.assertFalse(result)
-        self.assertTrue(manager.committed)
-
-    def test___exit__no_exc_nonretryable_commit_exception(self):
-        manager = DummyManager(raise_on_commit=ValueError)
-        inst = self._makeOne(manager)
-        self.assertRaises(ValueError, inst.__exit__, None, None, None)
-        self.assertTrue(manager.committed)
-        self.assertTrue(manager.aborted)
-
-    def test___exit__no_exc_abort_exception_after_nonretryable_commit_exc(self):
-        manager = DummyManager(raise_on_abort=ValueError, 
-                               raise_on_commit=KeyError)
-        inst = self._makeOne(manager)
-        self.assertRaises(ValueError, inst.__exit__, None, None, None)
-        self.assertTrue(manager.committed)
-        self.assertTrue(manager.aborted)
-        
-    def test___exit__no_exc_retryable_commit_exception(self):
-        from transaction.interfaces import TransientError
-        manager = DummyManager(raise_on_commit=TransientError)
-        inst = self._makeOne(manager)
-        result = inst.__exit__(None, None, None)
-        self.assertTrue(result)
-        self.assertTrue(manager.committed)
-        self.assertTrue(manager.aborted)
-
-    def test___exit__with_exception_value_retryable(self):
-        from transaction.interfaces import TransientError
-        manager = DummyManager()
-        inst = self._makeOne(manager)
-        result = inst.__exit__(TransientError, TransientError(), None)
-        self.assertTrue(result)
-        self.assertFalse(manager.committed)
-        self.assertTrue(manager.aborted)
-
-    def test___exit__with_exception_value_nonretryable(self):
-        manager = DummyManager()
-        inst = self._makeOne(manager)
-        self.assertRaises(KeyError, inst.__exit__, KeyError, KeyError(), None)
-        self.assertFalse(manager.committed)
-        self.assertTrue(manager.aborted)
-        
-class DummyManager(object):
-    entered = False
-    committed = False
-    aborted = False
-    
-    def __init__(self, raise_on_commit=None, raise_on_abort=None):
-        self.raise_on_commit = raise_on_commit
-        self.raise_on_abort = raise_on_abort
-
-    def _retryable(self, t, v):
-        from transaction._manager import TransientError
-        return issubclass(t, TransientError)
-        
-    def __enter__(self):
-        self.entered = True
-
-    def abort(self):
-        self.aborted = True
-        if self.raise_on_abort:
-            raise self.raise_on_abort
-        
-    def commit(self):
-        self.committed = True
-        if self.raise_on_commit:
-            raise self.raise_on_commit

Modified: transaction/trunk/transaction/tests/test_register_compat.py
===================================================================
--- transaction/trunk/transaction/tests/test_register_compat.py	2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/tests/test_register_compat.py	2012-12-18 05:26:55 UTC (rev 128757)
@@ -23,77 +23,64 @@
 These tests use a TestConnection object that implements the old API.
 They check that the right methods are called and in roughly the right
 order.
+"""
+import unittest
 
-Common cases
-------------
 
-First, check that a basic transaction commit works.
+class BBBTests(unittest.TestCase):
 
->>> cn = TestConnection()
->>> cn.register(Object())
->>> cn.register(Object())
->>> cn.register(Object())
->>> transaction.commit()
->>> len(cn.committed)
-3
->>> len(cn.aborted)
-0
->>> cn.calls
-['begin', 'vote', 'finish']
+    def setUp(self):
+        from transaction import abort
+        abort()
+    tearDown = setUp
 
-Second, check that a basic transaction abort works.  If the
-application calls abort(), then the transaction never gets into the
-two-phase commit.  It just aborts each object.
+    def test_basic_commit(self):
+        import transaction
+        cn = TestConnection()
+        cn.register(Object())
+        cn.register(Object())
+        cn.register(Object())
+        transaction.commit()
+        self.assertEqual(len(cn.committed), 3)
+        self.assertEqual(len(cn.aborted), 0)
+        self.assertEqual(cn.calls, ['begin', 'vote', 'finish'])
 
->>> cn = TestConnection()
->>> cn.register(Object())
->>> cn.register(Object())
->>> cn.register(Object())
->>> transaction.abort()
->>> len(cn.committed)
-0
->>> len(cn.aborted)
-3
->>> cn.calls
-[]
+    def test_basic_abort(self):
+        # If the application calls abort(), then the transaction never gets
+        # into the two-phase commit.  It just aborts each object.
+        import transaction
+        cn = TestConnection()
+        cn.register(Object())
+        cn.register(Object())
+        cn.register(Object())
+        transaction.abort()
+        self.assertEqual(len(cn.committed), 0)
+        self.assertEqual(len(cn.aborted), 3)
+        self.assertEqual(cn.calls, [])
 
-Error handling
---------------
+    def test_tpc_error(self):
+        # The tricky part of the implementation is recovering from an error
+        # that occurs during the two-phase commit.  We override the commit()
+        # and abort() methods of Object to cause errors during commit.
 
-The tricky part of the implementation is recovering from an error that
-occurs during the two-phase commit.  We override the commit() and
-abort() methods of Object to cause errors during commit.
+        # Note that the implementation uses lists internally, so that objects
+        # are committed in the order they are registered.  (In the presence
+        # of multiple resource managers, objects from a single resource
+        # manager are committed in order.  I'm not sure if this is an
+        # accident of the implementation or a feature that should be
+        # supported by any implementation.)
 
-Note that the implementation uses lists internally, so that objects
-are committed in the order they are registered.  (In the presence of
-multiple resource managers, objects from a single resource manager are
-committed in order.  I'm not sure if this is an accident of the
-implementation or a feature that should be supported by any
-implementation.)
+        # The order of resource managers depends on sortKey().
+        import transaction
+        cn = TestConnection()
+        cn.register(Object())
+        cn.register(CommitError())
+        cn.register(Object())
+        self.assertRaises(RuntimeError, transaction.commit)
+        self.assertEqual(len(cn.committed), 1)
+        self.assertEqual(len(cn.aborted), 3)
 
-The order of resource managers depends on sortKey().
 
->>> cn = TestConnection()
->>> cn.register(Object())
->>> cn.register(CommitError())
->>> cn.register(Object())
->>> transaction.commit()
-Traceback (most recent call last):
- ...
-RuntimeError: commit
->>> len(cn.committed)
-1
->>> len(cn.aborted)
-3
-
-Clean up:
-
->>> transaction.abort()
-"""
-
-import doctest
-import transaction
-
 class Object(object):
 
     def commit(self):
@@ -102,27 +89,32 @@
     def abort(self):
         pass
 
+
 class CommitError(Object):
 
     def commit(self):
         raise RuntimeError("commit")
 
+
 class AbortError(Object):
 
     def abort(self):
         raise RuntimeError("abort")
 
+
 class BothError(CommitError, AbortError):
     pass
 
-class TestConnection:
 
+class TestConnection(object):
+
     def __init__(self):
         self.committed = []
         self.aborted = []
         self.calls = []
 
     def register(self, obj):
+        import transaction
         obj._p_jar = self
         transaction.get().register(obj)
 
@@ -150,7 +142,6 @@
         self.aborted.append(obj)
 
 def test_suite():
-    return doctest.DocTestSuite()
-
-# additional_tests is for setuptools "setup.py test" support
-additional_tests = test_suite
+    return unittest.TestSuite((
+        unittest.makeSuite(BBBTests),
+    ))

Modified: transaction/trunk/transaction/tests/test_savepoint.py
===================================================================
--- transaction/trunk/transaction/tests/test_savepoint.py	2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/tests/test_savepoint.py	2012-12-18 05:26:55 UTC (rev 128757)
@@ -11,82 +11,56 @@
 # FOR A PARTICULAR PURPOSE.
 #
 ##############################################################################
-"""Tests of savepoint feature
-"""
 import unittest
-import doctest
 
 
-def testRollbackRollsbackDataManagersThatJoinedLater():
-    """
+class SavepointTests(unittest.TestCase):
 
-A savepoint needs to not just rollback it's savepoints, but needs to
-rollback savepoints for data managers that joined savepoints after the
-savepoint:
+    def testRollbackRollsbackDataManagersThatJoinedLater(self):
+        # A savepoint needs to not just rollback it's savepoints, but needs
+        # to # rollback savepoints for data managers that joined savepoints
+        # after the savepoint:
+        import transaction
+        from transaction.tests import savepointsample
+        dm = savepointsample.SampleSavepointDataManager()
+        dm['name'] = 'bob'
+        sp1 = transaction.savepoint()
+        dm['job'] = 'geek'
+        sp2 = transaction.savepoint()
+        dm['salary'] = 'fun'
+        dm2 = savepointsample.SampleSavepointDataManager()
+        dm2['name'] = 'sally'
 
-    >>> import transaction
-    >>> from transaction.tests import savepointsample
-    >>> dm = savepointsample.SampleSavepointDataManager()
-    >>> dm['name'] = 'bob'
-    >>> sp1 = transaction.savepoint()
-    >>> dm['job'] = 'geek'
-    >>> sp2 = transaction.savepoint()
-    >>> dm['salary'] = 'fun'
-    >>> dm2 = savepointsample.SampleSavepointDataManager()
-    >>> dm2['name'] = 'sally'
+        self.assertTrue('name' in dm)
+        self.assertTrue('job' in dm)
+        self.assertTrue('salary' in dm)
+        self.assertTrue('name' in dm2)
 
-    >>> 'name' in dm
-    True
-    >>> 'job' in dm
-    True
-    >>> 'salary' in dm
-    True
-    >>> 'name' in dm2
-    True
+        sp1.rollback()
 
-    >>> sp1.rollback()
+        self.assertTrue('name' in dm)
+        self.assertFalse('job' in dm)
+        self.assertFalse('salary' in dm)
+        self.assertFalse('name' in dm2)
 
-    >>> 'name' in dm
-    True
-    >>> 'job' in dm
-    False
-    >>> 'salary' in dm
-    False
-    >>> 'name' in dm2
-    False
+    def test_commit_after_rollback_for_dm_that_joins_after_savepoint(self):
+        # There was a problem handling data managers that joined after a
+        # savepoint.  If the savepoint was rolled back and then changes
+        # made, the dm would end up being joined twice, leading to extra
+        # tpc calls and pain.
+        import transaction
+        from transaction.tests import savepointsample
+        sp = transaction.savepoint()
+        dm = savepointsample.SampleSavepointDataManager()
+        dm['name'] = 'bob'
+        sp.rollback()
+        dm['name'] = 'Bob'
+        transaction.commit()
+        self.assertEqual(dm['name'], 'Bob')
 
-"""
 
-def test_commit_after_rollback_for_dm_that_joins_after_savepoint():
-    """
 
-There was a problem handling data managers that joined after a
-savepoint.  If the savepoint was rolled back and then changes made,
-the dm would end up being joined twice, leading to extra tpc calls and pain.
-
-    >>> import transaction
-    >>> sp = transaction.savepoint()
-    >>> from transaction.tests import savepointsample
-    >>> dm = savepointsample.SampleSavepointDataManager()
-    >>> dm['name'] = 'bob'
-    >>> sp.rollback()
-    >>> dm['name'] = 'Bob'
-    >>> transaction.commit()
-    >>> dm['name']
-    'Bob'
-    """
-
-
-
 def test_suite():
     return unittest.TestSuite((
-        doctest.DocFileSuite('savepoint.txt'),
-        doctest.DocTestSuite(),
+            unittest.makeSuite(SavepointTests),
         ))
-
-# additional_tests is for setuptools "setup.py test" support
-additional_tests = test_suite
-
-if __name__ == '__main__':
-    unittest.main(defaultTest='test_suite')
-

Deleted: transaction/trunk/transaction/tests/test_transaction.py
===================================================================
--- transaction/trunk/transaction/tests/test_transaction.py	2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/tests/test_transaction.py	2012-12-18 05:26:55 UTC (rev 128757)
@@ -1,781 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2001, 2002, 2005 Zope Foundation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE
-#
-##############################################################################
-"""Test transaction behavior for variety of cases.
-
-I wrote these unittests to investigate some odd transaction
-behavior when doing unittests of integrating non sub transaction
-aware objects, and to insure proper txn behavior. these
-tests test the transaction system independent of the rest of the
-zodb.
-
-you can see the method calls to a jar by passing the
-keyword arg tracing to the modify method of a dataobject.
-the value of the arg is a prefix used for tracing print calls
-to that objects jar.
-
-the number of times a jar method was called can be inspected
-by looking at an attribute of the jar that is the method
-name prefixed with a c (count/check).
-
-i've included some tracing examples for tests that i thought
-were illuminating as doc strings below.
-
-TODO
-
-    add in tests for objects which are modified multiple times,
-    for example an object that gets modified in multiple sub txns.
-"""
-from doctest import DocTestSuite, DocFileSuite, IGNORE_EXCEPTION_DETAIL
-
-import struct
-import sys
-import unittest
-import transaction
-
-_ADDRESS_MASK = 256 ** struct.calcsize('P')
-def positive_id(obj):
-    """Return id(obj) as a non-negative integer."""
-
-    result = id(obj)
-    if result < 0:
-        result += _ADDRESS_MASK
-        assert result > 0
-    return result
-
-class TransactionTests(unittest.TestCase):
-
-    def setUp(self):
-        mgr = self.transaction_manager = transaction.TransactionManager()
-        self.sub1 = DataObject(mgr)
-        self.sub2 = DataObject(mgr)
-        self.sub3 = DataObject(mgr)
-        self.nosub1 = DataObject(mgr, nost=1)
-
-    # basic tests with two sub trans jars
-    # really we only need one, so tests for
-    # sub1 should identical to tests for sub2
-    def testTransactionCommit(self):
-
-        self.sub1.modify()
-        self.sub2.modify()
-
-        self.transaction_manager.commit()
-
-        assert self.sub1._p_jar.ccommit_sub == 0
-        assert self.sub1._p_jar.ctpc_finish == 1
-
-    def testTransactionAbort(self):
-
-        self.sub1.modify()
-        self.sub2.modify()
-
-        self.transaction_manager.abort()
-
-        assert self.sub2._p_jar.cabort == 1
-
-    def testTransactionNote(self):
-
-        t = self.transaction_manager.get()
-
-        t.note('This is a note.')
-        self.assertEqual(t.description, 'This is a note.')
-        t.note('Another.')
-        self.assertEqual(t.description, 'This is a note.\nAnother.')
-
-        t.abort()
-
-
-    # repeat adding in a nonsub trans jars
-
-    def testNSJTransactionCommit(self):
-
-        self.nosub1.modify()
-
-        self.transaction_manager.commit()
-
-        assert self.nosub1._p_jar.ctpc_finish == 1
-
-    def testNSJTransactionAbort(self):
-
-        self.nosub1.modify()
-
-        self.transaction_manager.abort()
-
-        assert self.nosub1._p_jar.ctpc_finish == 0
-        assert self.nosub1._p_jar.cabort == 1
-
-
-    ### Failure Mode Tests
-    #
-    # ok now we do some more interesting
-    # tests that check the implementations
-    # error handling by throwing errors from
-    # various jar methods
-    ###
-
-    # first the recoverable errors
-
-    def testExceptionInAbort(self):
-
-        self.sub1._p_jar = BasicJar(errors='abort')
-
-        self.nosub1.modify()
-        self.sub1.modify(nojar=1)
-        self.sub2.modify()
-
-        try:
-            self.transaction_manager.abort()
-        except TestTxnException: pass
-
-        assert self.nosub1._p_jar.cabort == 1
-        assert self.sub2._p_jar.cabort == 1
-
-    def testExceptionInCommit(self):
-
-        self.sub1._p_jar = BasicJar(errors='commit')
-
-        self.nosub1.modify()
-        self.sub1.modify(nojar=1)
-
-        try:
-            self.transaction_manager.commit()
-        except TestTxnException: pass
-
-        assert self.nosub1._p_jar.ctpc_finish == 0
-        assert self.nosub1._p_jar.ccommit == 1
-        assert self.nosub1._p_jar.ctpc_abort == 1
-
-    def testExceptionInTpcVote(self):
-
-        self.sub1._p_jar = BasicJar(errors='tpc_vote')
-
-        self.nosub1.modify()
-        self.sub1.modify(nojar=1)
-
-        try:
-            self.transaction_manager.commit()
-        except TestTxnException: pass
-
-        assert self.nosub1._p_jar.ctpc_finish == 0
-        assert self.nosub1._p_jar.ccommit == 1
-        assert self.nosub1._p_jar.ctpc_abort == 1
-        assert self.sub1._p_jar.ctpc_abort == 1
-
-    def testExceptionInTpcBegin(self):
-        """
-        ok this test reveals a bug in the TM.py
-        as the nosub tpc_abort there is ignored.
-
-        nosub calling method tpc_begin
-        nosub calling method commit
-        sub calling method tpc_begin
-        sub calling method abort
-        sub calling method tpc_abort
-        nosub calling method tpc_abort
-        """
-        self.sub1._p_jar = BasicJar(errors='tpc_begin')
-
-        self.nosub1.modify()
-        self.sub1.modify(nojar=1)
-
-        try:
-            self.transaction_manager.commit()
-        except TestTxnException:
-            pass
-
-        assert self.nosub1._p_jar.ctpc_abort == 1
-        assert self.sub1._p_jar.ctpc_abort == 1
-
-    def testExceptionInTpcAbort(self):
-        self.sub1._p_jar = BasicJar(errors=('tpc_abort', 'tpc_vote'))
-
-        self.nosub1.modify()
-        self.sub1.modify(nojar=1)
-
-        try:
-            self.transaction_manager.commit()
-        except TestTxnException:
-            pass
-
-        assert self.nosub1._p_jar.ctpc_abort == 1
-
-    # last test, check the hosing mechanism
-
-##    def testHoserStoppage(self):
-##        # It's hard to test the "hosed" state of the database, where
-##        # hosed means that a failure occurred in the second phase of
-##        # the two phase commit.  It's hard because the database can
-##        # recover from such an error if it occurs during the very first
-##        # tpc_finish() call of the second phase.
-
-##        for obj in self.sub1, self.sub2:
-##            j = HoserJar(errors='tpc_finish')
-##            j.reset()
-##            obj._p_jar = j
-##            obj.modify(nojar=1)
-
-##        try:
-##            transaction.commit()
-##        except TestTxnException:
-##            pass
-
-##        self.assert_(Transaction.hosed)
-
-##        self.sub2.modify()
-
-##        try:
-##            transaction.commit()
-##        except Transaction.POSException.TransactionError:
-##            pass
-##        else:
-##            self.fail("Hosed Application didn't stop commits")
-
-
-class Test_oid_repr(unittest.TestCase):
-    def _callFUT(self, oid):
-        from transaction._transaction import oid_repr
-        return oid_repr(oid)
-
-    def test_as_nonstring(self):
-        self.assertEqual(self._callFUT(123), '123')
-
-    def test_as_string_not_8_chars(self):
-        self.assertEqual(self._callFUT('a'), "'a'")
-
-    def test_as_string_z64(self):
-        s = '\0'*8
-        self.assertEqual(self._callFUT(s), '0x00')
-
-    def test_as_string_all_Fs(self):
-        s = '\1'*8
-        self.assertEqual(self._callFUT(s), '0x0101010101010101')
-
-class DataObject:
-
-    def __init__(self, transaction_manager, nost=0):
-        self.transaction_manager = transaction_manager
-        self.nost = nost
-        self._p_jar = None
-
-    def modify(self, nojar=0, tracing=0):
-        if not nojar:
-            if self.nost:
-                self._p_jar = BasicJar(tracing=tracing)
-            else:
-                self._p_jar = BasicJar(tracing=tracing)
-        self.transaction_manager.get().join(self._p_jar)
-
-class TestTxnException(Exception):
-    pass
-
-class BasicJar:
-
-    def __init__(self, errors=(), tracing=0):
-        if not isinstance(errors, tuple):
-            errors = errors,
-        self.errors = errors
-        self.tracing = tracing
-        self.cabort = 0
-        self.ccommit = 0
-        self.ctpc_begin = 0
-        self.ctpc_abort = 0
-        self.ctpc_vote = 0
-        self.ctpc_finish = 0
-        self.cabort_sub = 0
-        self.ccommit_sub = 0
-
-    def __repr__(self):
-        return "<%s %X %s>" % (self.__class__.__name__,
-                               positive_id(self),
-                               self.errors)
-
-    def sortKey(self):
-        # All these jars use the same sort key, and Python's list.sort()
-        # is stable.  These two
-        return self.__class__.__name__
-
-    def check(self, method):
-        if self.tracing:
-            print('%s calling method %s'%(str(self.tracing),method))
-
-        if method in self.errors:
-            raise TestTxnException("error %s" % method)
-
-    ## basic jar txn interface
-
-    def abort(self, *args):
-        self.check('abort')
-        self.cabort += 1
-
-    def commit(self, *args):
-        self.check('commit')
-        self.ccommit += 1
-
-    def tpc_begin(self, txn, sub=0):
-        self.check('tpc_begin')
-        self.ctpc_begin += 1
-
-    def tpc_vote(self, *args):
-        self.check('tpc_vote')
-        self.ctpc_vote += 1
-
-    def tpc_abort(self, *args):
-        self.check('tpc_abort')
-        self.ctpc_abort += 1
-
-    def tpc_finish(self, *args):
-        self.check('tpc_finish')
-        self.ctpc_finish += 1
-
-class HoserJar(BasicJar):
-
-    # The HoserJars coordinate their actions via the class variable
-    # committed.  The check() method will only raise its exception
-    # if committed > 0.
-
-    committed = 0
-
-    def reset(self):
-        # Calling reset() on any instance will reset the class variable.
-        HoserJar.committed = 0
-
-    def check(self, method):
-        if HoserJar.committed > 0:
-            BasicJar.check(self, method)
-
-    def tpc_finish(self, *args):
-        self.check('tpc_finish')
-        self.ctpc_finish += 1
-        HoserJar.committed += 1
-
-
-def test_join():
-    """White-box test of the join method
-
-    The join method is provided for "backward-compatability" with ZODB 4
-    data managers.
-
-    The argument to join must be a zodb4 data manager,
-    transaction.interfaces.IDataManager.
-
-    >>> from transaction.tests.sampledm import DataManager
-    >>> from transaction._transaction import DataManagerAdapter
-    >>> t = transaction.Transaction()
-    >>> dm = DataManager()
-    >>> t.join(dm)
-
-    The end result is that a data manager adapter is one of the
-    transaction's objects:
-
-    >>> isinstance(t._resources[0], DataManagerAdapter)
-    True
-    >>> t._resources[0]._datamanager is dm
-    True
-
-    """
-
-def hook():
-    pass
-
-def test_addBeforeCommitHook():
-    """Test addBeforeCommitHook.
-
-    Let's define a hook to call, and a way to see that it was called.
-
-      >>> log = []
-      >>> def reset_log():
-      ...     del log[:]
-
-      >>> def hook(arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
-      ...     log.append("arg %r kw1 %r kw2 %r" % (arg, kw1, kw2))
-
-    Now register the hook with a transaction.
-
-      >>> from transaction.compat import func_name
-      >>> import transaction
-      >>> t = transaction.begin()
-      >>> t.addBeforeCommitHook(hook, '1')
-
-    We can see that the hook is indeed registered.
-
-      >>> [(func_name(hook), args, kws)
-      ...  for hook, args, kws in t.getBeforeCommitHooks()]
-      [('hook', ('1',), {})]
-
-    When transaction commit starts, the hook is called, with its
-    arguments.
-
-      >>> log
-      []
-      >>> t.commit()
-      >>> log
-      ["arg '1' kw1 'no_kw1' kw2 'no_kw2'"]
-      >>> reset_log()
-
-    A hook's registration is consumed whenever the hook is called.  Since
-    the hook above was called, it's no longer registered:
-
-      >>> len(list(t.getBeforeCommitHooks()))
-      0
-      >>> transaction.commit()
-      >>> log
-      []
-
-    The hook is only called for a full commit, not for a savepoint.
-
-      >>> t = transaction.begin()
-      >>> t.addBeforeCommitHook(hook, 'A', dict(kw1='B'))
-      >>> dummy = t.savepoint()
-      >>> log
-      []
-      >>> t.commit()
-      >>> log
-      ["arg 'A' kw1 'B' kw2 'no_kw2'"]
-      >>> reset_log()
-
-    If a transaction is aborted, no hook is called.
-
-      >>> t = transaction.begin()
-      >>> t.addBeforeCommitHook(hook, ["OOPS!"])
-      >>> transaction.abort()
-      >>> log
-      []
-      >>> transaction.commit()
-      >>> log
-      []
-
-    The hook is called before the commit does anything, so even if the
-    commit fails the hook will have been called.  To provoke failures in
-    commit, we'll add failing resource manager to the transaction.
-
-      >>> class CommitFailure(Exception):
-      ...     pass
-      >>> class FailingDataManager:
-      ...     def tpc_begin(self, txn, sub=False):
-      ...         raise CommitFailure('failed')
-      ...     def abort(self, txn):
-      ...         pass
-
-      >>> t = transaction.begin()
-      >>> t.join(FailingDataManager())
-
-      >>> t.addBeforeCommitHook(hook, '2')
-      >>> t.commit() #doctest: +IGNORE_EXCEPTION_DETAIL
-      Traceback (most recent call last):
-      ...
-      CommitFailure: failed
-      >>> log
-      ["arg '2' kw1 'no_kw1' kw2 'no_kw2'"]
-      >>> reset_log()
-
-    Let's register several hooks.
-
-      >>> t = transaction.begin()
-      >>> t.addBeforeCommitHook(hook, '4', dict(kw1='4.1'))
-      >>> t.addBeforeCommitHook(hook, '5', dict(kw2='5.2'))
-
-    They are returned in the same order by getBeforeCommitHooks.
-
-      >>> [(func_name(hook), args, kws)  #doctest: +NORMALIZE_WHITESPACE
-      ...  for hook, args, kws in t.getBeforeCommitHooks()]
-      [('hook', ('4',), {'kw1': '4.1'}),
-       ('hook', ('5',), {'kw2': '5.2'})]
-
-    And commit also calls them in this order.
-
-      >>> t.commit()
-      >>> len(log)
-      2
-      >>> log  #doctest: +NORMALIZE_WHITESPACE
-      ["arg '4' kw1 '4.1' kw2 'no_kw2'",
-       "arg '5' kw1 'no_kw1' kw2 '5.2'"]
-      >>> reset_log()
-
-    While executing, a hook can itself add more hooks, and they will all
-    be called before the real commit starts.
-
-      >>> def recurse(txn, arg):
-      ...     log.append('rec' + str(arg))
-      ...     if arg:
-      ...         txn.addBeforeCommitHook(hook, '-')
-      ...         txn.addBeforeCommitHook(recurse, (txn, arg-1))
-
-      >>> t = transaction.begin()
-      >>> t.addBeforeCommitHook(recurse, (t, 3))
-      >>> transaction.commit()
-      >>> log  #doctest: +NORMALIZE_WHITESPACE
-      ['rec3',
-               "arg '-' kw1 'no_kw1' kw2 'no_kw2'",
-       'rec2',
-               "arg '-' kw1 'no_kw1' kw2 'no_kw2'",
-       'rec1',
-               "arg '-' kw1 'no_kw1' kw2 'no_kw2'",
-       'rec0']
-      >>> reset_log()
-
-    """
-
-def test_addAfterCommitHook():
-    """Test addAfterCommitHook.
-
-    Let's define a hook to call, and a way to see that it was called.
-
-      >>> log = []
-      >>> def reset_log():
-      ...     del log[:]
-
-      >>> def hook(status, arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
-      ...     log.append("%r arg %r kw1 %r kw2 %r" % (status, arg, kw1, kw2))
-
-    Now register the hook with a transaction.
-
-      >>> from transaction.compat import func_name
-      >>> import transaction
-      >>> t = transaction.begin()
-      >>> t.addAfterCommitHook(hook, '1')
-
-    We can see that the hook is indeed registered.
-
-      >>> [(func_name(hook), args, kws)
-      ...  for hook, args, kws in t.getAfterCommitHooks()]
-      [('hook', ('1',), {})]
-
-    When transaction commit is done, the hook is called, with its
-    arguments.
-
-      >>> log
-      []
-      >>> t.commit()
-      >>> log
-      ["True arg '1' kw1 'no_kw1' kw2 'no_kw2'"]
-      >>> reset_log()
-
-    A hook's registration is consumed whenever the hook is called.  Since
-    the hook above was called, it's no longer registered:
-
-      >>> len(list(t.getAfterCommitHooks()))
-      0
-      >>> transaction.commit()
-      >>> log
-      []
-
-    The hook is only called after a full commit, not for a savepoint.
-
-      >>> t = transaction.begin()
-      >>> t.addAfterCommitHook(hook, 'A', dict(kw1='B'))
-      >>> dummy = t.savepoint()
-      >>> log
-      []
-      >>> t.commit()
-      >>> log
-      ["True arg 'A' kw1 'B' kw2 'no_kw2'"]
-      >>> reset_log()
-
-    If a transaction is aborted, no hook is called.
-
-      >>> t = transaction.begin()
-      >>> t.addAfterCommitHook(hook, ["OOPS!"])
-      >>> transaction.abort()
-      >>> log
-      []
-      >>> transaction.commit()
-      >>> log
-      []
-
-    The hook is called after the commit is done, so even if the
-    commit fails the hook will have been called.  To provoke failures in
-    commit, we'll add failing resource manager to the transaction.
-
-      >>> class CommitFailure(Exception):
-      ...     pass
-      >>> class FailingDataManager:
-      ...     def tpc_begin(self, txn):
-      ...         raise CommitFailure('failed')
-      ...     def abort(self, txn):
-      ...         pass
-
-      >>> t = transaction.begin()
-      >>> t.join(FailingDataManager())
-
-      >>> t.addAfterCommitHook(hook, '2')
-      >>> t.commit() #doctest: +IGNORE_EXCEPTION_DETAIL
-      Traceback (most recent call last):
-      ...
-      CommitFailure: failed
-      >>> log
-      ["False arg '2' kw1 'no_kw1' kw2 'no_kw2'"]
-      >>> reset_log()
-
-    Let's register several hooks.
-
-      >>> t = transaction.begin()
-      >>> t.addAfterCommitHook(hook, '4', dict(kw1='4.1'))
-      >>> t.addAfterCommitHook(hook, '5', dict(kw2='5.2'))
-
-    They are returned in the same order by getAfterCommitHooks.
-
-      >>> [(func_name(hook), args, kws)     #doctest: +NORMALIZE_WHITESPACE
-      ...  for hook, args, kws in t.getAfterCommitHooks()]
-      [('hook', ('4',), {'kw1': '4.1'}),
-       ('hook', ('5',), {'kw2': '5.2'})]
-
-    And commit also calls them in this order.
-
-      >>> t.commit()
-      >>> len(log)
-      2
-      >>> log  #doctest: +NORMALIZE_WHITESPACE
-      ["True arg '4' kw1 '4.1' kw2 'no_kw2'",
-       "True arg '5' kw1 'no_kw1' kw2 '5.2'"]
-      >>> reset_log()
-
-    While executing, a hook can itself add more hooks, and they will all
-    be called before the real commit starts.
-
-      >>> def recurse(status, txn, arg):
-      ...     log.append('rec' + str(arg))
-      ...     if arg:
-      ...         txn.addAfterCommitHook(hook, '-')
-      ...         txn.addAfterCommitHook(recurse, (txn, arg-1))
-
-      >>> t = transaction.begin()
-      >>> t.addAfterCommitHook(recurse, (t, 3))
-      >>> transaction.commit()
-      >>> log  #doctest: +NORMALIZE_WHITESPACE
-      ['rec3',
-               "True arg '-' kw1 'no_kw1' kw2 'no_kw2'",
-       'rec2',
-               "True arg '-' kw1 'no_kw1' kw2 'no_kw2'",
-       'rec1',
-               "True arg '-' kw1 'no_kw1' kw2 'no_kw2'",
-       'rec0']
-      >>> reset_log()
-
-    If an after commit hook is raising an exception then it will log a
-    message at error level so that if other hooks are registered they
-    can be executed. We don't support execution dependencies at this level.
-
-      >>> mgr = transaction.TransactionManager()
-      >>> do = DataObject(mgr)
-
-      >>> def hookRaise(status, arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
-      ...     raise TypeError("Fake raise")
-
-      >>> t = transaction.begin()
-
-      >>> t.addAfterCommitHook(hook, ('-', 1))
-      >>> t.addAfterCommitHook(hookRaise, ('-', 2))
-      >>> t.addAfterCommitHook(hook, ('-', 3))
-      >>> transaction.commit()
-
-      >>> log
-      ["True arg '-' kw1 1 kw2 'no_kw2'", "True arg '-' kw1 3 kw2 'no_kw2'"]
-
-      >>> reset_log()
-
-    Test that the associated transaction manager has been cleanup when
-    after commit hooks are registered
-
-      >>> mgr = transaction.TransactionManager()
-      >>> do = DataObject(mgr)
-
-      >>> t = transaction.begin()
-      >>> t._manager._txn is not None
-      True
-
-      >>> t.addAfterCommitHook(hook, ('-', 1))
-      >>> transaction.commit()
-
-      >>> log
-      ["True arg '-' kw1 1 kw2 'no_kw2'"]
-
-      >>> t._manager._txn is not None
-      False
-
-      >>> reset_log()
-    """
-
-def bug239086():
-    """
-    The original implementation of thread transaction manager made
-    invalid assumptions about thread ids.
-
-    >>> import transaction.tests.savepointsample
-    >>> dm = transaction.tests.savepointsample.SampleSavepointDataManager()
-    >>> list(dm.keys())
-    []
-
-    >>> class Sync:
-    ...      def __init__(self, label):
-    ...          self.label = label
-    ...      def beforeCompletion(self, t):
-    ...          print('%s %s' % (self.label, 'before'))
-    ...      def afterCompletion(self, t):
-    ...          print('%s %s' % (self.label, 'after'))
-    ...      def newTransaction(self, t):
-    ...          print('%s %s' % (self.label, 'new'))
-    >>> sync = Sync(1)
-
-    >>> import threading
-    >>> def run_in_thread(f):
-    ...     t = threading.Thread(target=f)
-    ...     t.start()
-    ...     t.join()
-
-    >>> @run_in_thread
-    ... def first():
-    ...     transaction.manager.registerSynch(sync)
-    ...     transaction.manager.begin()
-    ...     dm['a'] = 1
-    1 new
-
-    >>> @run_in_thread
-    ... def second():
-    ...     transaction.abort() # should do nothing.
-
-    >>> list(dm.keys())
-    ['a']
-
-    >>> dm = transaction.tests.savepointsample.SampleSavepointDataManager()
-    >>> list(dm.keys())
-    []
-
-    >>> @run_in_thread
-    ... def first():
-    ...     dm['a'] = 1
-
-    >>> transaction.abort() # should do nothing
-    >>> list(dm.keys())
-    ['a']
-
-    """
-
-def test_suite():
-    suite = unittest.TestSuite((
-        DocFileSuite('doom.txt'),
-        DocTestSuite(),
-        unittest.makeSuite(TransactionTests),
-        unittest.makeSuite(Test_oid_repr),
-        ))
-    if sys.version_info >= (2, 6):
-        suite.addTest(DocFileSuite('convenience.txt',
-                      optionflags=IGNORE_EXCEPTION_DETAIL))
-
-    return suite
-
-# additional_tests is for setuptools "setup.py test" support
-additional_tests = test_suite
-
-if __name__ == '__main__':
-    unittest.TextTestRunner().run(test_suite())

Modified: transaction/trunk/transaction/tests/test_weakset.py
===================================================================
--- transaction/trunk/transaction/tests/test_weakset.py	2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/tests/test_weakset.py	2012-12-18 05:26:55 UTC (rev 128757)
@@ -11,15 +11,12 @@
 # FOR A PARTICULAR PURPOSE
 #
 ##############################################################################
-
 import unittest
-from transaction.weakset import WeakSet
 
-class Dummy:
-    pass
 
 class WeakSetTests(unittest.TestCase):
     def test_contains(self):
+        from transaction.weakset import WeakSet
         w = WeakSet()
         dummy = Dummy()
         w.add(dummy)
@@ -29,6 +26,7 @@
 
     def test_len(self):
         import gc
+        from transaction.weakset import WeakSet
         w = WeakSet()
         d1 = Dummy()
         d2 = Dummy()
@@ -40,6 +38,7 @@
         self.assertEqual(len(w), 1)
 
     def test_remove(self):
+        from transaction.weakset import WeakSet
         w = WeakSet()
         dummy = Dummy()
         w.add(dummy)
@@ -49,6 +48,7 @@
 
     def test_as_weakref_list(self):
         import gc
+        from transaction.weakset import WeakSet
         w = WeakSet()
         dummy = Dummy()
         dummy2 = Dummy()
@@ -64,6 +64,7 @@
         self.assertEqual(set(L), set([dummy, dummy2]))
 
     def test_map(self):
+        from transaction.weakset import WeakSet
         w = WeakSet()
         dummy = Dummy()
         dummy2 = Dummy()
@@ -77,10 +78,10 @@
         for thing in dummy, dummy2, dummy3:
             self.assertEqual(thing.poked, 1)
         
+
+class Dummy:
+    pass
         
+
 def test_suite():
     return unittest.makeSuite(WeakSetTests)
-
-if __name__ == '__main__':
-    unittest.main()
-    



More information about the checkins mailing list