diff --git a/CHANGES.rst b/CHANGES.rst
index 4077211..e9d4cf3 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -18,6 +18,12 @@
   if it is only an instance of `BaseException` but not of `Exception`,
   such as e.g. a ``SystemExit`` or ``KeyboardInterupt`` exception.
 
+- Support abort hooks (symmetrically to commit hooks)
+  (`#77 <https://github.com/zopefoundation/transaction/issues/77>`_).
+
+- Hooks are now cleared after successfull ``commit`` and ``abort`` to avoid
+  potential cyclic references.
+
 
 2.4.0 (2018-10-23)
 ==================
diff --git a/transaction/_compat.py b/transaction/_compat.py
index 4a4ab0b..8d2e87b 100644
--- a/transaction/_compat.py
+++ b/transaction/_compat.py
@@ -4,23 +4,23 @@ import sys
 PY3 = sys.version_info[0] == 3
 JYTHON = sys.platform.startswith('java')
 
-if PY3:
+if PY3:  # pragma: no cover
     text_type = str
 else: # pragma: no cover
     # py2
     text_type = unicode
 
 def bytes_(s, encoding='latin-1', errors='strict'):
-    if isinstance(s, text_type):
+    if isinstance(s, text_type):  # pragma: no cover
         s = s.encode(encoding, errors)
     return s
 
 def text_(s):
-    if not isinstance(s, text_type):
+    if not isinstance(s, text_type):  # pragma: no cover
         s = s.decode('utf-8')
     return s
 
-if PY3:
+if PY3:  # pragma: no cover
     def native_(s, encoding='latin-1', errors='strict'):
         if isinstance(s, text_type):
             return s
@@ -31,7 +31,7 @@ else:  # pragma: no cover
             return s.encode(encoding, errors)
         return str(s)
 
-if PY3:
+if PY3:  # pragma: no cover
     from io import StringIO
 else: # pragma: no cover
     from io import BytesIO
@@ -43,7 +43,7 @@ else: # pragma: no cover
             super(StringIO, self).write(s)
 
 
-if PY3:
+if PY3:  # pragma: no cover
     def reraise(tp, value, tb=None):
         if value.__traceback__ is not tb: # pragma: no cover
             raise value.with_traceback(tb)
@@ -67,7 +67,7 @@ else: # pragma: no cover
 """)
 
 
-try:
+try:  # pragma: no cover
     from threading import get_ident as get_thread_ident
 except ImportError: # pragma: no cover
     # py2
diff --git a/transaction/_transaction.py b/transaction/_transaction.py
index 7fcbe65..9f1cca2 100644
--- a/transaction/_transaction.py
+++ b/transaction/_transaction.py
@@ -120,6 +120,12 @@ class Transaction(object):
         # List of (hook, args, kws) tuples added by addAfterCommitHook().
         self._after_commit = []
 
+        # List of (hook, args, kws) tuples added by addBeforeAbortHook().
+        self._before_abort = []
+
+        # List of (hook, args, kws) tuples added by addAfterAbortHook().
+        self._after_abort = []
+
     @property
     def _extension(self):
         # for backward compatibility, since most clients used this
@@ -312,9 +318,9 @@ class Transaction(object):
             finally:
                 del t, v, tb
         else:
-            self._free()
             self._synchronizers.map(lambda s: s.afterCompletion(self))
             self._callAfterCommitHooks(status=True)
+            self._free()
         self.log.debug("commit")
 
     def _saveAndGetCommitishError(self):
@@ -360,12 +366,8 @@ class Transaction(object):
 
     def _callBeforeCommitHooks(self):
         # Call all hooks registered, allowing further registrations
-        # during processing.  Note that calls to addBeforeCommitHook() may
-        # add additional hooks while hooks are running, and iterating over a
-        # growing list is well-defined in Python.
-        for hook, args, kws in self._before_commit:
-            hook(*args, **kws)
-        self._before_commit = []
+        # during processing.
+        self._call_hooks(self._before_commit)
 
     def getAfterCommitHooks(self):
         """ See ITransaction.
@@ -380,34 +382,85 @@ class Transaction(object):
         self._after_commit.append((hook, tuple(args), kws))
 
     def _callAfterCommitHooks(self, status=True):
+        self._call_hooks(self._after_commit,
+                         exc=False, clean=True, prefix_args=(status,))
+
+    def _call_hooks(self, hooks, exc=True, clean=False, prefix_args=()):
+        """call *hooks*.
+
+        If *exc* is true, fail on the first exception; otherwise
+        log the exception and continue.
+
+        If *clean* is true, abort all resources. This is to ensure
+        a clean state should a (after) hook has affected one
+        of the resources.
+
+        *prefix_args* defines additioan arguments prefixed
+        to the arguments provided by the hook definition.
+
+        ``_call_hooks`` supports that a hook adds new hooks.
+        """
         # Avoid to abort anything at the end if no hooks are registred.
-        if not self._after_commit:
+        if not hooks:
             return
+        try:
+            # Call all hooks registered, allowing further registrations
+            # during processing
+            for hook, args, kws in hooks:
+                try:
+                    hook(*(prefix_args + args), **kws)
+                except:
+                    if exc:
+                        raise
+                    # We should not fail
+                    self.log.error("Error in hook exec in %s ",
+                                   hook, exc_info=sys.exc_info())
+        finally:
+            del hooks[:]  # clear hooks
+            if not clean:
+                return
+            # The primary operation has already been performed.
+            # But the hooks execution might have left the resources
+            # in an unclean state. Clean up
+            for rm in self._resources:
+                try:
+                    rm.abort(self)
+                except:
+                    # XXX should we take further actions here ?
+                    self.log.error("Error in abort() on manager %s",
+                                   rm, exc_info=sys.exc_info())
+
+    def getBeforeAbortHooks(self):
+        """ See ITransaction.
+        """
+        return iter(self._before_abort)
+
+    def addBeforeAbortHook(self, hook, args=(), kws=None):
+        """ See ITransaction.
+        """
+        if kws is None:
+            kws = {}
+        self._before_abort.append((hook, tuple(args), kws))
+
+    def _callBeforeAbortHooks(self):
         # Call all hooks registered, allowing further registrations
-        # during processing.  Note that calls to addAterCommitHook() may
-        # add additional hooks while hooks are running, and iterating over a
-        # growing list is well-defined in Python.
-        for hook, args, kws in self._after_commit:
-            # The first argument passed to the hook is a Boolean value,
-            # true if the commit succeeded, or false if the commit aborted.
-            try:
-                hook(status, *args, **kws)
-            except:
-                # We need to catch the exceptions if we want all hooks
-                # to be called
-                self.log.error("Error in after commit hook exec in %s ",
-                               hook, exc_info=sys.exc_info())
-        # The transaction is already committed. It must not have
-        # further effects after the commit.
-        for rm in self._resources:
-            try:
-                rm.abort(self)
-            except:
-                # XXX should we take further actions here ?
-                self.log.error("Error in abort() on manager %s",
-                               rm, exc_info=sys.exc_info())
-        self._after_commit = []
-        self._before_commit = []
+        # during processing.
+        self._call_hooks(self._before_abort, exc=False)
+
+    def getAfterAbortHooks(self):
+        """ See ITransaction.
+        """
+        return iter(self._after_abort)
+
+    def addAfterAbortHook(self, hook, args=(), kws=None):
+        """ See ITransaction.
+        """
+        if kws is None:
+            kws = {}
+        self._after_abort.append((hook, tuple(args), kws))
+
+    def _callAfterAbortHooks(self):
+        self._call_hooks(self._after_abort, clean=True)
 
     def _commitResources(self):
         # Execute the two-phase commit protocol.
@@ -469,6 +522,7 @@ class Transaction(object):
         # to break references---this transaction object will not be returned
         # as the current transaction from its manager after this, and all
         # IDatamanager objects joined to it will forgotten
+        # All hooks are forgotten.
         if self._manager:
             self._manager.free(self)
 
@@ -477,6 +531,11 @@ class Transaction(object):
 
         del self._resources[:]
 
+        del self._before_commit[:]
+        del self._after_commit[:]
+        del self._before_abort[:]
+        del self._after_abort[:]
+
     def data(self, ob):
         try:
             data = self._data
@@ -499,6 +558,7 @@ class Transaction(object):
     def abort(self):
         """ See ITransaction.
         """
+        self._callBeforeAbortHooks()
         if self._savepoint2index:
             self._invalidate_all_savepoints()
 
@@ -519,6 +579,7 @@ class Transaction(object):
                     self.log.error("Failed to abort resource manager: %s",
                                    rm, exc_info=sys.exc_info())
 
+            self._callAfterAbortHooks()
             self._free()
 
             self._synchronizers.map(lambda s: s.afterCompletion(self))
diff --git a/transaction/interfaces.py b/transaction/interfaces.py
index 85ce458..85e859c 100644
--- a/transaction/interfaces.py
+++ b/transaction/interfaces.py
@@ -356,6 +356,78 @@ class ITransaction(Interface):
         by a top-level transaction commit.
         """
 
+    def addBeforeAbortHook(hook, args=(), kws=None):
+        """Register a hook to call before the transaction is abortted.
+
+        The specified hook function will be called after the transaction's
+        abort method has been called, but before the abort process has been
+        started.  The hook will be passed the specified positional (`args`)
+        and keyword (`kws`) arguments.  `args` is a sequence of positional
+        arguments to be passed, defaulting to an empty tuple (no positional
+        arguments are passed).  `kws` is a dictionary of keyword argument
+        names and values to be passed, or the default None (no keyword
+        arguments are passed).
+
+        Multiple hooks can be registered and will be called in the order they
+        were registered (first registered, first called).  This method can
+        also be called from a hook:  an executing hook can register more
+        hooks.  Applications should take care to avoid creating infinite loops
+        by recursively registering hooks.
+
+        Abort hooks are called only for a top-level abort. If the
+        transaction is committed, abort hooks are not called.
+        This is true even if the commit fails. In this case, however,
+        the transaction is in the ``COMMITFAILED`` state and is
+        virtually unusable; therefore, a top-level abort will typically
+        follow.
+
+        Calling a hook "consumes" its registration.
+        Hook registrations do not persist across transactions.
+        """
+
+    def getBeforeAbortHooks():
+        """Return iterable producing the registered addBeforeAbort hooks.
+
+        A triple (hook, args, kws) is produced for each registered hook.
+        The hooks are produced in the order in which they would be invoked
+        by a top-level transaction abort.
+        """
+
+    def addAfterAbortHook(hook, args=(), kws=None):
+        """Register a hook to call after a transaction abort.
+
+        The specified hook function will be called after the transaction
+        abort  with positional arguments `args`  and `kws`
+        keyword arguments.  `args` is a sequence of
+        positional arguments to be passed, defaulting to an empty tuple
+        `kws` is a dictionary of keyword argument names and values to be
+        passed, or the default None (no keyword arguments are passed).
+        
+        Multiple hooks can be registered and will be called in the order they
+        were registered (first registered, first called).  This method can
+        also be called from a hook:  an executing hook can register more
+        hooks.  Applications should take care to avoid creating infinite loops
+        by recursively registering hooks.
+
+        Abort hooks are called only for a top-level abort. If the
+        transaction is committed, abort hooks are not called.
+        This is true even if the commit fails. In this case, however,
+        the transaction is in the ``COMMITFAILED`` state and is
+        virtually unusable; therefore, a top-level abort will typically
+        follow.
+
+        Calling a hook "consumes" its registration.
+        Hook registrations do not persist across transactions.
+        """
+
+    def getAfterAbortHooks():
+        """Return iterable producing the registered addAfterAbort hooks.
+
+        A triple (hook, args, kws) is produced for each registered hook.
+        The hooks are produced in the order in which they would be invoked
+        by a top-level transaction abort.
+        """
+
     def set_data(ob, data):
         """Hold data on behalf of an object
 

Support abort hooks (symmetrically to commit hooks)
`transaction` currently supports (before and after) commit hooks. It should symmetrically support abort hooks as well.

I need this feature in `dm.zodb.asynchronous`, more precisely in its `TransactionalScheduler`. This scheduler allows to schedule a function call at transaction commit; it uses a commit hook for this. If the transaction is aborted (instead of committed), a cleanup becomes necessary. This requires a (transaction) abort hook.

I implement this feature currently in `dm.transaction.aborthook` via monkey patching. If there is interest, I could implement it instead in `transaction`.
diff --git a/transaction/tests/test__transaction.py b/transaction/tests/test__transaction.py
index 575a663..fb8e5d4 100644
--- a/transaction/tests/test__transaction.py
+++ b/transaction/tests/test__transaction.py
@@ -615,7 +615,7 @@ class TransactionTests(unittest.TestCase):
         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"))
+                            "Error in hook"))
 
     def test_callAfterCommitHook_w_abort(self):
         from transaction.tests.common import DummyLogger
@@ -635,7 +635,7 @@ class TransactionTests(unittest.TestCase):
         txn._callAfterCommitHooks()
         self.assertEqual(logger._log[0][0], 'error')
         self.assertTrue(logger._log[0][1].startswith(
-                            "Error in after commit hook"))
+                            "Error in hook"))
 
     def test__commitResources_normal(self):
         from transaction.tests.common import DummyLogger
@@ -855,9 +855,8 @@ class TransactionTests(unittest.TestCase):
             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, (), {})])
+        # Hooks are not called but cleared on abort
+        self.assertEqual(list(txn.getBeforeCommitHooks()), [])
 
     def test_abort_w_synchronizers(self):
         from transaction.weakset import WeakSet
@@ -899,14 +898,13 @@ class TransactionTests(unittest.TestCase):
             txn._after_commit.append((_hook2, (), {}))
             logger._clear()
             txn.abort()
-        # Hooks are neither called nor cleared on abort
+        # Hooks are not called but cleared on abort
         self.assertEqual(_hooked1, [])
         self.assertEqual(_hooked2, [])
-        self.assertEqual(list(txn.getAfterCommitHooks()),
-                         [(_hook1, ('one',), {'uno': 1}), (_hook2, (), {})])
+        self.assertEqual(list(txn.getAfterCommitHooks()), [])
         self.assertEqual(txn._resources, [])
 
-    def test_abort_error_w_afterCompleteHooks(self):
+    def test_abort_error_w_afterCommitHooks(self):
         from transaction import _transaction
         from transaction.tests.common import DummyLogger
         from transaction.tests.common import Monkey
@@ -933,11 +931,10 @@ class TransactionTests(unittest.TestCase):
             txn._resources.append(broken2)
             logger._clear()
             self.assertRaises(ValueError, txn.abort)
-        # Hooks are neither called nor cleared on abort
+        # Hooks are not called but cleared on abort
         self.assertEqual(_hooked1, [])
         self.assertEqual(_hooked2, [])
-        self.assertEqual(list(txn.getAfterCommitHooks()),
-                         [(_hook1, ('one',), {'uno': 1}), (_hook2, (), {})])
+        self.assertEqual(list(txn.getAfterCommitHooks()), [])
         self.assertTrue(aaa._a)
         self.assertFalse(aaa._x)
 
@@ -983,6 +980,178 @@ class TransactionTests(unittest.TestCase):
         txn.abort()
         self.assertEqual(txn._resources, [])
 
+    def test_getBeforeAbortHooks_empty(self):
+        txn = self._makeOne()
+        self.assertEqual(list(txn.getBeforeAbortHooks()), [])
+
+    def test_addBeforeAbortHook(self):
+        def _hook(*args, **kw):
+            raise AssertionError("Not called")
+        txn = self._makeOne()
+        txn.addBeforeAbortHook(_hook, ('one',), dict(uno=1))
+        self.assertEqual(list(txn.getBeforeAbortHooks()),
+                         [(_hook, ('one',), {'uno': 1})])
+
+    def test_addBeforeAbortHook_w_kws(self):
+        def _hook(*args, **kw):
+            raise AssertionError("Not called")
+        txn = self._makeOne()
+        txn.addBeforeAbortHook(_hook, ('one',))
+        self.assertEqual(list(txn.getBeforeAbortHooks()),
+                         [(_hook, ('one',), {})])
+
+    def test_getAfterAbortHooks_empty(self):
+        txn = self._makeOne()
+        self.assertEqual(list(txn.getAfterAbortHooks()), [])
+
+    def test_addAfterAbortHook(self):
+        def _hook(*args, **kw):
+            raise AssertionError("Not called")
+        txn = self._makeOne()
+        txn.addAfterAbortHook(_hook, ('one',), dict(uno=1))
+        self.assertEqual(list(txn.getAfterAbortHooks()),
+                         [(_hook, ('one',), {'uno': 1})])
+
+    def test_addAfterAbortHook_wo_kws(self):
+        def _hook(*args, **kw):
+            raise AssertionError("Not called")
+        txn = self._makeOne()
+        txn.addAfterAbortHook(_hook, ('one',))
+        self.assertEqual(list(txn.getAfterAbortHooks()),
+                         [(_hook, ('one',), {})])
+
+    def test_callBeforeAbortHook_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.addBeforeAbortHook(_hook1, ('one',))
+        txn.addBeforeAbortHook(_hook2, ('two',), dict(dos=2))
+        txn._callBeforeAbortHooks()
+        # second hook gets called even if first raises
+        self.assertEqual(_hooked2, [(('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 hook"))
+
+    def test_callBeforeAbortHook_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.addBeforeAbortHook(_hook1, ('one',))
+        txn.addBeforeAbortHook(_hook2, ('two',), dict(dos=2))
+        txn._callBeforeAbortHooks()
+        self.assertEqual(logger._log[0][0], 'error')
+        self.assertTrue(logger._log[0][1].startswith(
+                            "Error in hook"))
+
+    def test_callAfterAbortHook_w_abort_error(self):
+        from transaction.tests.common import DummyLogger
+        from transaction.tests.common import Monkey
+        from transaction import _transaction
+        _hooked2 = []
+        def _hook2(*args, **kw):
+            _hooked2.append((args, kw))
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            txn = self._makeOne()
+        logger._clear()
+        r = Resource("r", "abort")
+        txn.join(r)
+        txn.addAfterAbortHook(_hook2, ('two',), dict(dos=2))
+        txn._callAfterAbortHooks()
+        self.assertEqual(logger._log[0][0], 'error')
+        self.assertTrue(logger._log[0][1].startswith(
+                            "Error in abort() on manager"))
+
+    def test_callAfterAbortHook_w_error_w_abort_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))  # pragma: no cover
+        logger = DummyLogger()
+        with Monkey(_transaction, _LOGGER=logger):
+            txn = self._makeOne()
+        logger._clear()
+        r = Resource("r", "abort")
+        txn.join(r)
+        txn.addAfterAbortHook(_hook1, ('one',), dict(dos=1))
+        txn.addAfterAbortHook(_hook2, ('two',), dict(dos=2))
+        with self.assertRaises(ValueError):
+            txn._callAfterAbortHooks()
+        self.assertEqual(logger._log[0][0], 'error')
+        self.assertTrue(logger._log[0][1].startswith(
+                            "Error in abort() on manager"))
+
+    def test_abort_w_abortHooks(self):
+        comm = []
+        txn = self._makeOne()
+        def bah():
+            comm.append("before")
+        def aah():
+            comm.append("after")
+        txn.addAfterAbortHook(aah)
+        txn.addBeforeAbortHook(bah)
+        txn.abort()
+        self.assertEqual(comm, ["before", "after"])
+        self.assertEqual(list(txn.getBeforeAbortHooks()), [])
+        self.assertEqual(list(txn.getAfterAbortHooks()), [])
+        
+    def test_commit_w_abortHooks(self):
+        comm = []
+        txn = self._makeOne()
+        def bah():
+            comm.append("before")  # pragma: no cover
+        def aah():
+            comm.append("after")  # pragma: no cover
+        txn.addAfterAbortHook(aah)
+        txn.addBeforeAbortHook(bah)
+        txn.commit()
+        self.assertEqual(comm, [])  # not called
+        # but cleared
+        self.assertEqual(list(txn.getBeforeAbortHooks()), [])
+        self.assertEqual(list(txn.getAfterAbortHooks()), [])
+        
+    def test_commit_w_error_w_abortHooks(self):
+        comm = []
+        txn = self._makeOne()
+        def bah():
+            comm.append("before")  # pragma: no cover
+        def aah():
+            comm.append("after")  # pragma: no cover
+        txn.addAfterAbortHook(aah)
+        txn.addBeforeAbortHook(bah)
+        r = Resource("aaa", "tpc_vote")
+        txn.join(r)
+        with self.assertRaises(ValueError):
+            txn.commit()
+        self.assertEqual(comm, [])  # not called
+        # not cleared
+        self.assertEqual(list(txn.getBeforeAbortHooks()), [(bah, (), {})])
+        self.assertEqual(list(txn.getAfterAbortHooks()), [(aah, (), {})])
+        
     def test_note(self):
         txn = self._makeOne()
         try:

pip install -e .[dev]
parse_log_pytest
pytest
zope.testrunner coverage pytest
3.7
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs @ file:///croot/attrs_1668696182826/work
certifi @ file:///croot/certifi_1671487769961/work/certifi
coverage==7.2.7
flit_core @ file:///opt/conda/conda-bld/flit-core_1644941570762/work/source/flit_core
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1648562407465/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
packaging @ file:///croot/packaging_1671697413597/work
pluggy @ file:///tmp/build/80754af9/pluggy_1648042572264/work
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pytest==7.1.2
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
-e git+https://github.com/zopefoundation/transaction.git@b1579531afafae7a9b0e85f88e5508ead587ad5f#egg=transaction
typing_extensions @ file:///croot/typing_extensions_1669924550328/work
zipp @ file:///croot/zipp_1672387121353/work
zope.exceptions==5.1
zope.interface==6.4.post2
zope.testrunner==6.5

name: transaction
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=22.1.0=py37h06a4308_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - flit-core=3.6.0=pyhd3eb1b0_0
  - importlib-metadata=4.11.3=py37h06a4308_0
  - importlib_metadata=4.11.3=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=22.0=py37h06a4308_0
  - pip=22.3.1=py37h06a4308_0
  - pluggy=1.0.0=py37h06a4308_1
  - py=1.11.0=pyhd3eb1b0_0
  - pytest=7.1.2=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py37h06a4308_0
  - typing_extensions=4.4.0=py37h06a4308_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.11.0=py37h06a4308_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - coverage==7.2.7
      - zope-exceptions==5.1
      - zope-interface==6.4.post2
      - zope-testrunner==6.5
prefix: /opt/conda/envs/transaction

transaction/tests/test__transaction.py::TransactionTests::test_abort_error_w_afterCommitHooks transaction/tests/test__transaction.py::TransactionTests::test_abort_w_abortHooks transaction/tests/test__transaction.py::TransactionTests::test_abort_w_afterCommitHooks transaction/tests/test__transaction.py::TransactionTests::test_abort_w_beforeCommitHooks transaction/tests/test__transaction.py::TransactionTests::test_addAfterAbortHook transaction/tests/test__transaction.py::TransactionTests::test_addAfterAbortHook_wo_kws transaction/tests/test__transaction.py::TransactionTests::test_addBeforeAbortHook transaction/tests/test__transaction.py::TransactionTests::test_addBeforeAbortHook_w_kws transaction/tests/test__transaction.py::TransactionTests::test_callAfterAbortHook_w_abort_error transaction/tests/test__transaction.py::TransactionTests::test_callAfterAbortHook_w_error_w_abort_error transaction/tests/test__transaction.py::TransactionTests::test_callAfterCommitHook_w_abort transaction/tests/test__transaction.py::TransactionTests::test_callAfterCommitHook_w_error transaction/tests/test__transaction.py::TransactionTests::test_callBeforeAbortHook_w_abort transaction/tests/test__transaction.py::TransactionTests::test_callBeforeAbortHook_w_error transaction/tests/test__transaction.py::TransactionTests::test_commit_w_abortHooks transaction/tests/test__transaction.py::TransactionTests::test_commit_w_error_w_abortHooks transaction/tests/test__transaction.py::TransactionTests::test_getAfterAbortHooks_empty transaction/tests/test__transaction.py::TransactionTests::test_getBeforeAbortHooks_empty
transaction/tests/test__transaction.py::TransactionTests::test__commitResources_error_in_afterCompletion transaction/tests/test__transaction.py::TransactionTests::test__commitResources_error_in_commit transaction/tests/test__transaction.py::TransactionTests::test__commitResources_error_in_tpc_begin transaction/tests/test__transaction.py::TransactionTests::test__commitResources_error_in_tpc_finish transaction/tests/test__transaction.py::TransactionTests::test__commitResources_error_in_tpc_vote transaction/tests/test__transaction.py::TransactionTests::test__commitResources_normal transaction/tests/test__transaction.py::TransactionTests::test__extension_settable transaction/tests/test__transaction.py::TransactionTests::test__invalidate_all_savepoints transaction/tests/test__transaction.py::TransactionTests::test__prior_operation_failed transaction/tests/test__transaction.py::TransactionTests::test__remove_and_invalidate_after_hit transaction/tests/test__transaction.py::TransactionTests::test__remove_and_invalidate_after_miss transaction/tests/test__transaction.py::TransactionTests::test__unjoin_hit transaction/tests/test__transaction.py::TransactionTests::test__unjoin_miss transaction/tests/test__transaction.py::TransactionTests::test_abort_clears_resources transaction/tests/test__transaction.py::TransactionTests::test_abort_error_w_synchronizers transaction/tests/test__transaction.py::TransactionTests::test_abort_w_savepoints transaction/tests/test__transaction.py::TransactionTests::test_abort_w_synchronizers transaction/tests/test__transaction.py::TransactionTests::test_abort_wo_savepoints_wo_hooks_wo_synchronizers transaction/tests/test__transaction.py::TransactionTests::test_addAfterCommitHook transaction/tests/test__transaction.py::TransactionTests::test_addAfterCommitHook_wo_kws transaction/tests/test__transaction.py::TransactionTests::test_addBeforeCommitHook transaction/tests/test__transaction.py::TransactionTests::test_addBeforeCommitHook_w_kws transaction/tests/test__transaction.py::TransactionTests::test_commit_COMMITFAILED transaction/tests/test__transaction.py::TransactionTests::test_commit_DOOMED transaction/tests/test__transaction.py::TransactionTests::test_commit_clears_resources transaction/tests/test__transaction.py::TransactionTests::test_commit_error_w_afterCompleteHooks transaction/tests/test__transaction.py::TransactionTests::test_commit_error_w_synchronizers transaction/tests/test__transaction.py::TransactionTests::test_commit_w_afterCommitHooks transaction/tests/test__transaction.py::TransactionTests::test_commit_w_beforeCommitHooks transaction/tests/test__transaction.py::TransactionTests::test_commit_w_savepoints transaction/tests/test__transaction.py::TransactionTests::test_commit_w_synchronizers transaction/tests/test__transaction.py::TransactionTests::test_commit_wo_savepoints_wo_hooks_wo_synchronizers transaction/tests/test__transaction.py::TransactionTests::test_ctor_defaults transaction/tests/test__transaction.py::TransactionTests::test_ctor_w_syncs transaction/tests/test__transaction.py::TransactionTests::test_data transaction/tests/test__transaction.py::TransactionTests::test_description_42 transaction/tests/test__transaction.py::TransactionTests::test_description_None transaction/tests/test__transaction.py::TransactionTests::test_description_bytes transaction/tests/test__transaction.py::TransactionTests::test_doom_active transaction/tests/test__transaction.py::TransactionTests::test_doom_already_doomed transaction/tests/test__transaction.py::TransactionTests::test_doom_invalid transaction/tests/test__transaction.py::TransactionTests::test_getAfterCommitHooks_empty transaction/tests/test__transaction.py::TransactionTests::test_getBeforeCommitHooks_empty transaction/tests/test__transaction.py::TransactionTests::test_isDoomed transaction/tests/test__transaction.py::TransactionTests::test_isRetryableError_w_multiple transaction/tests/test__transaction.py::TransactionTests::test_isRetryableError_w_normal_exception_no_resources transaction/tests/test__transaction.py::TransactionTests::test_isRetryableError_w_normal_exception_w_resource_voting_yes transaction/tests/test__transaction.py::TransactionTests::test_isRetryableError_w_transient_error transaction/tests/test__transaction.py::TransactionTests::test_isRetryableError_w_transient_subclass transaction/tests/test__transaction.py::TransactionTests::test_join_ACTIVE_w_preparing_w_sp2index transaction/tests/test__transaction.py::TransactionTests::test_join_COMMITFAILED transaction/tests/test__transaction.py::TransactionTests::test_join_COMMITTED transaction/tests/test__transaction.py::TransactionTests::test_join_COMMITTING transaction/tests/test__transaction.py::TransactionTests::test_join_DOOMED_non_preparing_wo_sp2index transaction/tests/test__transaction.py::TransactionTests::test_note transaction/tests/test__transaction.py::TransactionTests::test_note_42 transaction/tests/test__transaction.py::TransactionTests::test_note_None transaction/tests/test__transaction.py::TransactionTests::test_note_bytes transaction/tests/test__transaction.py::TransactionTests::test_register_w_jar transaction/tests/test__transaction.py::TransactionTests::test_register_w_jar_already_adapted transaction/tests/test__transaction.py::TransactionTests::test_register_wo_jar transaction/tests/test__transaction.py::TransactionTests::test_savepoint_COMMITFAILED transaction/tests/test__transaction.py::TransactionTests::test_savepoint_empty transaction/tests/test__transaction.py::TransactionTests::test_savepoint_non_optimistc_resource_wo_support transaction/tests/test__transaction.py::TransactionTests::test_setExtendedInfo_multiple transaction/tests/test__transaction.py::TransactionTests::test_setExtendedInfo_single transaction/tests/test__transaction.py::TransactionTests::test_setUser_default_path transaction/tests/test__transaction.py::TransactionTests::test_setUser_explicit_path transaction/tests/test__transaction.py::TransactionTests::test_user_non_text transaction/tests/test__transaction.py::TransactionTests::test_user_w_none transaction/tests/test__transaction.py::TransactionTests::test_verifyImplements_ITransaction transaction/tests/test__transaction.py::TransactionTests::test_verifyProvides_ITransaction transaction/tests/test__transaction.py::MultiObjectResourceAdapterTests::test___repr__ transaction/tests/test__transaction.py::MultiObjectResourceAdapterTests::test_abort transaction/tests/test__transaction.py::MultiObjectResourceAdapterTests::test_abort_w_error transaction/tests/test__transaction.py::MultiObjectResourceAdapterTests::test_commit transaction/tests/test__transaction.py::MultiObjectResourceAdapterTests::test_ctor transaction/tests/test__transaction.py::MultiObjectResourceAdapterTests::test_sortKey transaction/tests/test__transaction.py::MultiObjectResourceAdapterTests::test_tpc_abort transaction/tests/test__transaction.py::MultiObjectResourceAdapterTests::test_tpc_begin transaction/tests/test__transaction.py::MultiObjectResourceAdapterTests::test_tpc_finish transaction/tests/test__transaction.py::MultiObjectResourceAdapterTests::test_tpc_vote transaction/tests/test__transaction.py::Test_rm_key::test_hit transaction/tests/test__transaction.py::Test_rm_key::test_miss transaction/tests/test__transaction.py::Test_object_hint::test_hit transaction/tests/test__transaction.py::Test_object_hint::test_miss transaction/tests/test__transaction.py::Test_oid_repr::test_as_nonstring transaction/tests/test__transaction.py::Test_oid_repr::test_as_string_all_Fs transaction/tests/test__transaction.py::Test_oid_repr::test_as_string_not_8_chars transaction/tests/test__transaction.py::Test_oid_repr::test_as_string_xxx transaction/tests/test__transaction.py::Test_oid_repr::test_as_string_z64 transaction/tests/test__transaction.py::DataManagerAdapterTests::test_abort transaction/tests/test__transaction.py::DataManagerAdapterTests::test_commit transaction/tests/test__transaction.py::DataManagerAdapterTests::test_ctor transaction/tests/test__transaction.py::DataManagerAdapterTests::test_sortKey transaction/tests/test__transaction.py::DataManagerAdapterTests::test_tpc_abort transaction/tests/test__transaction.py::DataManagerAdapterTests::test_tpc_begin transaction/tests/test__transaction.py::DataManagerAdapterTests::test_tpc_finish transaction/tests/test__transaction.py::DataManagerAdapterTests::test_tpc_vote transaction/tests/test__transaction.py::SavepointTests::test_ctor_w_savepoint_aware_resources transaction/tests/test__transaction.py::SavepointTests::test_ctor_w_savepoint_oblivious_resource_non_optimistic transaction/tests/test__transaction.py::SavepointTests::test_ctor_w_savepoint_oblivious_resource_optimistic transaction/tests/test__transaction.py::SavepointTests::test_rollback_w_sp_error transaction/tests/test__transaction.py::SavepointTests::test_rollback_w_txn_None transaction/tests/test__transaction.py::SavepointTests::test_valid_w_transacction transaction/tests/test__transaction.py::SavepointTests::test_valid_wo_transacction transaction/tests/test__transaction.py::AbortSavepointTests::test_ctor transaction/tests/test__transaction.py::AbortSavepointTests::test_rollback transaction/tests/test__transaction.py::NoRollbackSavepointTests::test_ctor transaction/tests/test__transaction.py::NoRollbackSavepointTests::test_rollback transaction/tests/test__transaction.py::MiscellaneousTests::test_BBB_join transaction/tests/test__transaction.py::MiscellaneousTests::test_bug239086 transaction/tests/test__transaction.py::MiscellaneousTests::test_gh5
transaction/_transaction.py transaction/_compat.py transaction/interfaces.py CHANGES.rst
transaction/_transaction.py transaction/_compat.py transaction/interfaces.py CHANGES.rst