[Checkins] SVN: transaction/branches/sphinx/ Move whole-file doctests into docs/.
Tres Seaver
cvs-admin at zope.org
Mon Dec 17 20:28:49 UTC 2012
Log message for revision 128696:
Move whole-file doctests into docs/.
Changed:
_U transaction/branches/sphinx/
A transaction/branches/sphinx/docs/convenience.rst
A transaction/branches/sphinx/docs/doom.rst
U transaction/branches/sphinx/docs/index.rst
A transaction/branches/sphinx/docs/savepoint.rst
D transaction/branches/sphinx/transaction/tests/convenience.txt
D transaction/branches/sphinx/transaction/tests/doom.txt
D transaction/branches/sphinx/transaction/tests/savepoint.txt
U transaction/branches/sphinx/transaction/tests/savepointsample.py
U transaction/branches/sphinx/transaction/tests/test_savepoint.py
U transaction/branches/sphinx/transaction/tests/test_transaction.py
-=-
Copied: transaction/branches/sphinx/docs/convenience.rst (from rev 128695, transaction/branches/sphinx/transaction/tests/convenience.txt)
===================================================================
--- transaction/branches/sphinx/docs/convenience.rst (rev 0)
+++ transaction/branches/sphinx/docs/convenience.rst 2012-12-17 20:28:48 UTC (rev 128696)
@@ -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/branches/sphinx/docs/doom.rst (from rev 128695, transaction/branches/sphinx/transaction/tests/doom.txt)
===================================================================
--- transaction/branches/sphinx/docs/doom.rst (rev 0)
+++ transaction/branches/sphinx/docs/doom.rst 2012-12-17 20:28:48 UTC (rev 128696)
@@ -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):
+ ...
+ AssertionError
+
+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()
Modified: transaction/branches/sphinx/docs/index.rst
===================================================================
--- transaction/branches/sphinx/docs/index.rst 2012-12-17 17:28:39 UTC (rev 128695)
+++ transaction/branches/sphinx/docs/index.rst 2012-12-17 20:28:48 UTC (rev 128696)
@@ -6,6 +6,9 @@
.. toctree::
:maxdepth: 2
+ convenience
+ doom
+ savepoint
api
Copied: transaction/branches/sphinx/docs/savepoint.rst (from rev 128695, transaction/branches/sphinx/transaction/tests/savepoint.txt)
===================================================================
--- transaction/branches/sphinx/docs/savepoint.rst (rev 0)
+++ transaction/branches/sphinx/docs/savepoint.rst 2012-12-17 20:28:48 UTC (rev 128696)
@@ -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'
+
Deleted: transaction/branches/sphinx/transaction/tests/convenience.txt
===================================================================
--- transaction/branches/sphinx/transaction/tests/convenience.txt 2012-12-17 17:28:39 UTC (rev 128695)
+++ transaction/branches/sphinx/transaction/tests/convenience.txt 2012-12-17 20:28:48 UTC (rev 128696)
@@ -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/branches/sphinx/transaction/tests/doom.txt
===================================================================
--- transaction/branches/sphinx/transaction/tests/doom.txt 2012-12-17 17:28:39 UTC (rev 128695)
+++ transaction/branches/sphinx/transaction/tests/doom.txt 2012-12-17 20:28:48 UTC (rev 128696)
@@ -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()
Deleted: transaction/branches/sphinx/transaction/tests/savepoint.txt
===================================================================
--- transaction/branches/sphinx/transaction/tests/savepoint.txt 2012-12-17 17:28:39 UTC (rev 128695)
+++ transaction/branches/sphinx/transaction/tests/savepoint.txt 2012-12-17 20:28:48 UTC (rev 128696)
@@ -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/branches/sphinx/transaction/tests/savepointsample.py
===================================================================
--- transaction/branches/sphinx/transaction/tests/savepointsample.py 2012-12-17 17:28:39 UTC (rev 128695)
+++ transaction/branches/sphinx/transaction/tests/savepointsample.py 2012-12-17 20:28:48 UTC (rev 128696)
@@ -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
Modified: transaction/branches/sphinx/transaction/tests/test_savepoint.py
===================================================================
--- transaction/branches/sphinx/transaction/tests/test_savepoint.py 2012-12-17 17:28:39 UTC (rev 128695)
+++ transaction/branches/sphinx/transaction/tests/test_savepoint.py 2012-12-17 20:28:48 UTC (rev 128696)
@@ -80,7 +80,6 @@
def test_suite():
return unittest.TestSuite((
- doctest.DocFileSuite('savepoint.txt'),
doctest.DocTestSuite(),
))
Modified: transaction/branches/sphinx/transaction/tests/test_transaction.py
===================================================================
--- transaction/branches/sphinx/transaction/tests/test_transaction.py 2012-12-17 17:28:39 UTC (rev 128695)
+++ transaction/branches/sphinx/transaction/tests/test_transaction.py 2012-12-17 20:28:48 UTC (rev 128696)
@@ -36,10 +36,8 @@
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
-
+from doctest import DocTestSuite
import struct
-import sys
import unittest
import transaction
@@ -763,14 +761,10 @@
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
More information about the checkins
mailing list