From 4e7eb29bd831d34dad37715bc58bf0f7a3b76c52 Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Mon, 9 Dec 2019 13:35:12 -0600 Subject: [PATCH] Remove ITransactionDeprecated and supporting code. Fixes #89. Has just enough documentation fixes from #88 to make the doctests pass on current versions of sphinx/docutils. --- CHANGES.rst | 5 + docs/convenience.rst | 3 - docs/integrations.rst | 3 + transaction/_compat.py | 5 - transaction/_transaction.py | 181 +----------- transaction/interfaces.py | 18 +- transaction/tests/test__transaction.py | 329 +--------------------- transaction/tests/test_register_compat.py | 142 ---------- 8 files changed, 17 insertions(+), 669 deletions(-) delete mode 100644 transaction/tests/test_register_compat.py diff --git a/CHANGES.rst b/CHANGES.rst index 8bebba7..5b8aae8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,11 @@ - Add support for Python 3.8. +- Drop support for legacy transaction APIs including + ``Transaction.register()`` and old ZODB3-style datamanagers. See + `issue 89 + `_. + - ``TransactionManager.run`` now commits/aborts the transaction "active" after the execution of *func* (and no longer the initial transaction which might already have been committed/aborted by *func*) diff --git a/docs/convenience.rst b/docs/convenience.rst index 5bbb92d..b9098c3 100644 --- a/docs/convenience.rst +++ b/docs/convenience.rst @@ -1,9 +1,6 @@ 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 ------------ diff --git a/docs/integrations.rst b/docs/integrations.rst index 85d9edd..338bff5 100644 --- a/docs/integrations.rst +++ b/docs/integrations.rst @@ -5,17 +5,20 @@ The following packages have been integrated with the ``transaction`` package so that their transactions can be integerated with others. `ZODB `_ + ZODB was the original user of the ``transaction`` package. Its transactions are controlled by ``transaction`` and ZODB fully implements the 2-phase commit protocol. `SQLAlchemy `_ + An Object Relational Mapper for Python, SQLAlchemy can use `zope.sqlalchemy `_ to have its transactions integrated with others. `repoze.sendmail `_ + repoze.sendmail allows coupling the sending of email messages with a transaction, using the Zope transaction manager. This allows messages to only be sent out when and if a transaction is committed, diff --git a/transaction/_compat.py b/transaction/_compat.py index 8d2e87b..607c21f 100644 --- a/transaction/_compat.py +++ b/transaction/_compat.py @@ -10,11 +10,6 @@ # py2 text_type = unicode -def bytes_(s, encoding='latin-1', errors='strict'): - if isinstance(s, text_type): # pragma: no cover - s = s.encode(encoding, errors) - return s - def text_(s): if not isinstance(s, text_type): # pragma: no cover s = s.decode('utf-8') diff --git a/transaction/_transaction.py b/transaction/_transaction.py index 4520362..9fe5628 100644 --- a/transaction/_transaction.py +++ b/transaction/_transaction.py @@ -11,7 +11,6 @@ # FOR A PARTICULAR PURPOSE. # ############################################################################ -import binascii import logging import sys import warnings @@ -25,8 +24,6 @@ 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 from transaction._compat import text_type @@ -45,11 +42,6 @@ def _makeLogger(): #pragma NO COVER 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): - return getattr(obj, attr, _marker) is not _marker - class Status(object): # ACTIVE is the initial state. ACTIVE = "Active" @@ -69,8 +61,7 @@ class _NoSynchronizers(object): def map(_f): "Does nothing" -@implementer(interfaces.ITransaction, - interfaces.ITransactionDeprecated) +@implementer(interfaces.ITransaction) class Transaction(object): @@ -197,12 +188,6 @@ def join(self, resource): # I think some users want it. raise ValueError("expected txn status %r or %r, but it's %r" % ( Status.ACTIVE, Status.DOOMED, self.status)) - # TODO: the prepare check is a bit of a hack, perhaps it would - # be better to use interfaces. If this is a ZODB4-style - # resource manager, it needs to be adapted, too. - if myhasattr(resource, "prepare"): - # TODO: deprecate 3.6 - resource = DataManagerAdapter(resource) self._resources.append(resource) if self._savepoint2index: @@ -265,33 +250,6 @@ def _invalidate_all_savepoints(self): savepoint.transaction = None # invalidate self._savepoint2index.clear() - - def register(self, obj): - """ See ITransaction. - """ - # The old way of registering transaction participants. - # - # register() is passed either a persistent object or a - # resource manager like the ones defined in ZODB.DB. - # If it is passed a persistent object, that object should - # 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") - adapter = self._adapters.get(manager) - if adapter is None: - adapter = MultiObjectResourceAdapter(manager) - adapter.objects.append(obj) - self._adapters[manager] = adapter - self.join(adapter) - else: - # TODO: comment out this expensive assert later - # Use id() to guard against proxies. - assert id(obj) not in map(id, adapter.objects) - adapter.objects.append(obj) - def commit(self): """ See ITransaction. """ @@ -649,145 +607,14 @@ def isRetryableError(self, error): # TODO: We need a better name for the adapters. -class MultiObjectResourceAdapter(object): - """Adapt the old-style register() call to the new-style join(). - - With join(), a resource manager like a Connection registers with - the transaction manager. With register(), an individual object - is passed to register(). - """ - def __init__(self, jar): - self.manager = jar - self.objects = [] - self.ncommitted = 0 - - def __repr__(self): - return "<%s for %s at %s>" % (self.__class__.__name__, - self.manager, id(self)) - - def sortKey(self): - return self.manager.sortKey() - - def tpc_begin(self, txn): - self.manager.tpc_begin(txn) - - def tpc_finish(self, txn): - self.manager.tpc_finish(txn) - - def tpc_abort(self, txn): - self.manager.tpc_abort(txn) - - def commit(self, txn): - for o in self.objects: - self.manager.commit(o, txn) - self.ncommitted += 1 - - def tpc_vote(self, txn): - self.manager.tpc_vote(txn) - - def abort(self, txn): - t = None - v = None - tb = None - try: - for o in self.objects: - try: - self.manager.abort(o, txn) - except: - # Capture the first exception and re-raise it after - # aborting all the other objects. - if tb is None: - t, v, tb = sys.exc_info() - txn.log.error("Failed to abort object: %s", - object_hint(o), exc_info=sys.exc_info()) - - if tb is not None: - reraise(t, v, tb) - finally: - del t, v, tb - - def rm_key(rm): func = getattr(rm, 'sortKey', None) if func is not None: return func() -def object_hint(o): - """Return a string describing the object. - - This function does not raise an exception. - """ - # We should always be able to get __class__. - klass = o.__class__.__name__ - # 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): - if isinstance(oid, str) and len(oid) == 8: - # Convert to hex and strip leading zeroes. - as_hex = native_( - binascii.hexlify(bytes_(oid, 'ascii')), 'ascii').lstrip('0') - # Ensure two characters per input byte. - if len(as_hex) & 1: - as_hex = '0' + as_hex - elif as_hex == '': - as_hex = '00' - return '0x' + as_hex - else: - return repr(oid) - - -# TODO: deprecate for 3.6. -class DataManagerAdapter(object): - """Adapt zodb 4-style data managers to zodb3 style - - Adapt transaction.interfaces.IDataManager to - ZODB.interfaces.IPureDatamanager - """ - - # Note that it is pretty important that this does not have a _p_jar - # attribute. This object will be registered with a zodb3 TM, which - # will then try to get a _p_jar from it, using it as the default. - # (Objects without a _p_jar are their own data managers.) - - def __init__(self, datamanager): - self._datamanager = datamanager - - # TODO: I'm not sure why commit() doesn't do anything - - def commit(self, transaction): - # We don't do anything here because ZODB4-style data managers - # didn't have a separate commit step - pass - - def abort(self, transaction): - self._datamanager.abort(transaction) - - def tpc_begin(self, transaction): - # We don't do anything here because ZODB4-style data managers - # didn't have a separate tpc_begin step - pass - - def tpc_abort(self, transaction): - self._datamanager.abort(transaction) - - def tpc_finish(self, transaction): - self._datamanager.commit(transaction) - - def tpc_vote(self, transaction): - self._datamanager.prepare(transaction) - - def sortKey(self): - return self._datamanager.sortKey() - @implementer(interfaces.ISavepoint) -class Savepoint: +class Savepoint(object): """Transaction savepoint. Transaction savepoints coordinate savepoints for data managers @@ -831,7 +658,7 @@ def rollback(self): transaction._saveAndRaiseCommitishError() # reraises! -class AbortSavepoint: +class AbortSavepoint(object): def __init__(self, datamanager, transaction): self.datamanager = datamanager @@ -842,7 +669,7 @@ def rollback(self): self.transaction._unjoin(self.datamanager) -class NoRollbackSavepoint: +class NoRollbackSavepoint(object): def __init__(self, datamanager): self.datamanager = datamanager diff --git a/transaction/interfaces.py b/transaction/interfaces.py index c02b2eb..0badc76 100644 --- a/transaction/interfaces.py +++ b/transaction/interfaces.py @@ -267,10 +267,10 @@ def note(text): def setExtendedInfo(name, value): """Add extension data to the transaction. - name + :param text name: is the text (unicode) name of the extension property to set - value + :param value: must be picklable and json serializable (not an instance). Multiple calls may be made to set multiple extension @@ -459,20 +459,6 @@ def isRetryableError(error): """ -class ITransactionDeprecated(Interface): - """Deprecated parts of the transaction API.""" - - def begin(info=None): - """Begin a new transaction. - - If the transaction is in progress, it is aborted and a new - transaction is started using the same transaction object. - """ - - # TODO: deprecate this for 3.6. - def register(object): - """Register the given object for transaction control.""" - class IDataManager(Interface): """Objects that manage transactional storage. diff --git a/transaction/tests/test__transaction.py b/transaction/tests/test__transaction.py index c5f5c9c..a0c774f 100644 --- a/transaction/tests/test__transaction.py +++ b/transaction/tests/test__transaction.py @@ -168,29 +168,6 @@ def test_join_DOOMED_non_preparing_wo_sp2index(self): 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): - raise AssertionError("Not called") - 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 @@ -308,46 +285,6 @@ def __repr__(self): # pragma: no cover 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 @@ -1006,7 +943,6 @@ def abort(self, txn): self.assertIsNot(t._synchronizers, ws) def test_abort_synchronizer_error_w_resources(self): - from transaction.weakset import WeakSet from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey from transaction import _transaction @@ -1437,123 +1373,6 @@ def should_retry(self, err): self.assertTrue(txn.isRetryableError(Exception())) - -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): # pragma: no cover - 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), - '' % 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 in ('b', 'c'): - 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)]) - - 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): @@ -1567,132 +1386,6 @@ 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') - - def test_as_string_xxx(self): - s = '\20'*8 - self.assertEqual(self._callFUT(s), '0x1010101010101010') - - -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): # pragma: no cover - 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): @@ -1829,22 +1522,6 @@ def test_rollback(self): 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. @@ -1872,14 +1549,14 @@ def run_in_thread(f): sync = Sync(1) @run_in_thread - def first(): + def _(): transaction.manager.registerSynch(sync) transaction.manager.begin() dm['a'] = 1 self.assertEqual(sync.log, ['1 new']) @run_in_thread - def second(): + def _(): transaction.abort() # should do nothing. self.assertEqual(sync.log, ['1 new']) self.assertEqual(list(dm.keys()), ['a']) @@ -1888,7 +1565,7 @@ def second(): self.assertEqual(list(dm.keys()), []) @run_in_thread - def third(): + def _(): dm['a'] = 1 self.assertEqual(sync.log, ['1 new']) diff --git a/transaction/tests/test_register_compat.py b/transaction/tests/test_register_compat.py deleted file mode 100644 index 83054d9..0000000 --- a/transaction/tests/test_register_compat.py +++ /dev/null @@ -1,142 +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. -# -############################################################################## -"""Test backwards compatibility for resource managers using register(). - -The transaction package supports several different APIs for resource -managers. The original ZODB3 API was implemented by ZODB.Connection. -The Connection passed persistent objects to a Transaction's register() -method. It's possible that third-party code also used this API, hence -these tests that the code that adapts the old interface to the current -API works. - -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 - - -class BBBTests(unittest.TestCase): - - def setUp(self): - from transaction import abort - abort() - tearDown = setUp - - 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']) - - 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, []) - - 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. - - # 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) - - -class Object(object): - - def commit(self): - pass - - def abort(self): - pass - - -class CommitError(Object): - - def commit(self): - raise RuntimeError("commit") - - -class AbortError(Object): - - def abort(self): - raise AssertionError("This should not actually be called") - - -class BothError(CommitError, AbortError): - pass - - -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) - - def sortKey(self): - return str(id(self)) - - def tpc_begin(self, txn): - self.calls.append("begin") - - def tpc_vote(self, txn): - self.calls.append("vote") - - def tpc_finish(self, txn): - self.calls.append("finish") - - def tpc_abort(self, txn): - self.calls.append("abort") - - def commit(self, obj, txn): - obj.commit() - self.committed.append(obj) - - def abort(self, obj, txn): - obj.abort() - self.aborted.append(obj)