diff -Nru transaction-2.4.0/CHANGES.rst transaction-3.0.0/CHANGES.rst --- transaction-2.4.0/CHANGES.rst 2018-10-23 14:27:09.000000000 +0000 +++ transaction-3.0.0/CHANGES.rst 2019-12-11 16:24:39.000000000 +0000 @@ -2,6 +2,42 @@ Changes ========= +3.0.0 (2019-12-11) +================== + +- Drop support for Python 3.4. + +- 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*) + (`#58 `_). + + It aborts the transaction now for all exceptions raised by *func* - even + 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 `_). + +- Make Transaction drop references to its hooks, manager, + synchronizers and data after a successful ``commit()`` and after + *any* ``abort()``. This helps avoid potential cyclic references. See + `issue 82 `_. + +- Allow synchronizers to access ``Transaction.data()`` when their + ``afterCompletion`` method is called while aborting a transaction. + +- Make it safe to call ``Transaction.abort()`` more than once. The + second and subsequent calls are no-ops. Previously a + ``ValueError(Foreign transaction)`` would be raised. + 2.4.0 (2018-10-23) ================== @@ -322,7 +358,7 @@ 1.0a1 (2007-12-18) ================== -= Initial release, branched from ZODB trunk on 2007-11-08 (aka +- Initial release, branched from ZODB trunk on 2007-11-08 (aka "3.9.0dev"). - Remove (deprecated) support for beforeCommitHook alias to diff -Nru transaction-2.4.0/debian/changelog transaction-3.0.0/debian/changelog --- transaction-2.4.0/debian/changelog 2019-09-06 10:37:35.000000000 +0000 +++ transaction-3.0.0/debian/changelog 2020-01-01 18:04:54.000000000 +0000 @@ -1,3 +1,21 @@ +transaction (3.0.0-1) unstable; urgency=medium + + * QA upload + * New upstream release + * d/watch secure URI + * d/control + - Secure URI on homepage + - Bump debhelper to 12 + - Update Standards-Version to 4.4.1 + - Add Rules-Requires-Root: no + * Change to debhelper-compat + * d/copyright + - Update to correct format URI + - Use secure URI's + * d/rules add PYBUILD_NAME + + -- HÃ¥vard Flaget Aasen Wed, 01 Jan 2020 19:04:54 +0100 + transaction (2.4.0-2) unstable; urgency=medium * QA upload. diff -Nru transaction-2.4.0/debian/compat transaction-3.0.0/debian/compat --- transaction-2.4.0/debian/compat 2014-08-11 10:32:08.000000000 +0000 +++ transaction-3.0.0/debian/compat 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -9 diff -Nru transaction-2.4.0/debian/control transaction-3.0.0/debian/control --- transaction-2.4.0/debian/control 2019-09-06 10:34:55.000000000 +0000 +++ transaction-3.0.0/debian/control 2020-01-01 18:04:54.000000000 +0000 @@ -2,14 +2,15 @@ Section: zope Priority: optional Maintainer: Debian QA Group -Build-Depends: debhelper (>= 8), +Build-Depends: debhelper-compat (= 12), dh-python, python3-all, python3-setuptools, python3-zope.interface, python3-mock, -Standards-Version: 4.4.0 -Homepage: http://pypi.python.org/pypi/transaction +Standards-Version: 4.4.1 +Rules-Requires-Root: no +Homepage: https://pypi.python.org/pypi/transaction Package: python3-transaction Section: python diff -Nru transaction-2.4.0/debian/copyright transaction-3.0.0/debian/copyright --- transaction-2.4.0/debian/copyright 2011-10-21 05:18:37.000000000 +0000 +++ transaction-3.0.0/debian/copyright 2020-01-01 17:45:12.000000000 +0000 @@ -1,7 +1,7 @@ -Format: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=174 +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: transaction Upstream-Contact: Zope Foundation and Contributors -Source: http://pypi.python.org/pypi/transaction +Source: https://pypi.python.org/pypi/transaction Files: * Copyright: (c) 2001-2007 Zope Foundation and Contributors. diff -Nru transaction-2.4.0/debian/rules transaction-3.0.0/debian/rules --- transaction-2.4.0/debian/rules 2019-09-06 07:48:40.000000000 +0000 +++ transaction-3.0.0/debian/rules 2020-01-01 18:04:14.000000000 +0000 @@ -1,4 +1,6 @@ #!/usr/bin/make -f +export PYBUILD_NAME=transaction + %: dh $@ --with python3 --buildsystem=pybuild diff -Nru transaction-2.4.0/debian/watch transaction-3.0.0/debian/watch --- transaction-2.4.0/debian/watch 2011-10-21 05:18:37.000000000 +0000 +++ transaction-3.0.0/debian/watch 2020-01-01 17:43:02.000000000 +0000 @@ -1,2 +1,2 @@ version=3 -http://pypi.python.org/packages/source/t/transaction/transaction-(.*)\.tar\.gz +https://pypi.python.org/packages/source/t/transaction/transaction-(.*)\.tar\.gz diff -Nru transaction-2.4.0/docs/api.rst transaction-3.0.0/docs/api.rst --- transaction-2.4.0/docs/api.rst 2018-10-23 14:27:09.000000000 +0000 +++ transaction-3.0.0/docs/api.rst 2019-12-11 16:24:39.000000000 +0000 @@ -1,81 +1,53 @@ -:mod:`transaction` API Reference -================================ +=============================== + ``transaction`` API Reference +=============================== Interfaces ----------- +========== .. module:: transaction.interfaces .. autointerface:: ITransactionManager - :members: - :member-order: bysource .. autointerface:: ITransaction - :members: - :member-order: bysource .. autointerface:: IDataManager - :members: - :member-order: bysource .. autointerface:: ISavepointDataManager - :members: - :member-order: bysource .. autointerface:: IRetryDataManager - :members: - :member-order: bysource .. autointerface:: IDataManagerSavepoint - :members: - :member-order: bysource .. autointerface:: ISavepoint - :members: - :member-order: bysource - -.. autoclass:: InvalidSavepointRollbackError - :members: - :member-order: bysource .. autointerface:: ISynchronizer - :members: - :member-order: bysource + +Exceptions +---------- .. autoclass:: TransactionError - :members: - :member-order: bysource .. autoclass:: TransactionFailedError - :members: - :member-order: bysource .. autoclass:: DoomedTransaction - :members: - :member-order: bysource .. autoclass:: TransientError - :members: - :member-order: bysource -API Objects ------------ +.. autoclass:: InvalidSavepointRollbackError -.. module:: transaction._transaction +.. autoclass:: NoTransaction -.. autoclass:: Transaction - :members: - :member-order: bysource +.. autoclass:: AlreadyInTransaction -.. autoclass:: Savepoint - :members: - :member-order: bysource +API Objects +=========== -.. module:: transaction._manager +.. automodule:: transaction + +.. autoclass:: Transaction .. autoclass:: TransactionManager - :members: - :member-order: bysource .. automethod:: __enter__ @@ -84,6 +56,7 @@ .. automethod:: __exit__ On error, aborts the current transaction. Otherwise, commits. + .. autoclass:: ThreadTransactionManager - :members: - :member-order: bysource + +.. autoclass:: Savepoint diff -Nru transaction-2.4.0/docs/changes.rst transaction-3.0.0/docs/changes.rst --- transaction-2.4.0/docs/changes.rst 1970-01-01 00:00:00.000000000 +0000 +++ transaction-3.0.0/docs/changes.rst 2019-12-11 16:24:39.000000000 +0000 @@ -0,0 +1 @@ +.. include:: ../CHANGES.rst diff -Nru transaction-2.4.0/docs/conf.py transaction-3.0.0/docs/conf.py --- transaction-2.4.0/docs/conf.py 2018-10-23 14:27:09.000000000 +0000 +++ transaction-3.0.0/docs/conf.py 2019-12-11 16:24:39.000000000 +0000 @@ -12,17 +12,24 @@ # serve to show the default. import sys, os +import pkg_resources # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) +sys.path.append(os.path.abspath('../')) +rqmt = pkg_resources.require('transaction')[0] # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' +# 1.8 was the last version that runs on Python 2; 2.0+ requires Python 3. +# `autodoc_default_options` was new in 1.8 +needs_sphinx = "1.8" + # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ @@ -30,6 +37,7 @@ 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx.ext.viewcode', + 'sphinx.ext.intersphinx', 'repoze.sphinx.autointerface', ] @@ -54,9 +62,9 @@ # built documents. # # The short X.Y version. -version = '1.2' +version = '%s.%s' % tuple(map(int, rqmt.version.split('.')[:2])) # The full version, including alpha/beta/rc tags. -release = '1.2' +release = rqmt.version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -73,7 +81,7 @@ exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +default_role = 'obj' # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True @@ -247,3 +255,17 @@ # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' + +# Sphinx 1.8+ prefers this to `autodoc_default_flags`. It's documented that +# either True or None mean the same thing as just setting the flag, but +# only None works in 1.8 (True works in 2.0) +autodoc_default_options = { + 'members': None, + 'show-inheritance': None, +} +autodoc_member_order = 'bysource' + +intersphinx_mapping = { + 'https://docs.python.org/': None, + 'http://www.zodb.org/en/latest/': None, +} diff -Nru transaction-2.4.0/docs/convenience.rst transaction-3.0.0/docs/convenience.rst --- transaction-2.4.0/docs/convenience.rst 2018-10-23 14:27:09.000000000 +0000 +++ transaction-3.0.0/docs/convenience.rst 2019-12-11 16:24:39.000000000 +0000 @@ -1,12 +1,9 @@ -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.) +================================= + Transaction convenience support +================================= with support ------------- +============ We can now use the with statement to define transaction boundaries. @@ -42,11 +39,11 @@ 3 On Python 2, you can also abbreviate ``with transaction.manager:`` as ``with -transaction:``. This does not work on Python 3 (see see +transaction:``. This does not work on Python 3 (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 @@ -66,7 +63,7 @@ Transaction managers provide two helpers for this case. Running and retrying functions as transactions -______________________________________________ +---------------------------------------------- The first helper runs a function as a transaction:: @@ -104,7 +101,7 @@ The default number of times to try is 3. Retrying code blocks using a attempt iterator -_____________________________________________ +--------------------------------------------- An older helper for running transactions uses an iterator of attempts:: diff -Nru transaction-2.4.0/docs/datamanager.rst transaction-3.0.0/docs/datamanager.rst --- transaction-2.4.0/docs/datamanager.rst 2018-10-23 14:27:09.000000000 +0000 +++ transaction-3.0.0/docs/datamanager.rst 2019-12-11 16:24:39.000000000 +0000 @@ -1,154 +1,297 @@ -Writing a Data Manager -====================== +======================== + Writing a Data Manager +======================== + +.. currentmodule:: transaction.interfaces Simple Data Manager -------------------- +=================== .. doctest:: >>> from transaction.tests.examples import DataManager -This :class:`transaction.tests.examples.DataManager` class -provides a trivial data-manager implementation and docstrings to illustrate -the the protocol and to provide a tool for writing tests. +This class provides a trivial `IDataManager` implementation and doc +strings to illustrate the protocol and to provide a tool for writing +tests. Our sample data manager has state that is updated through an inc method and through transaction operations. - When we create a sample data manager: .. doctest:: - >>> dm = DataManager() + >>> rm = DataManager() -It has two bits of state, state: +It has two pieces state, state and delta, both initialized to 0: .. doctest:: - >>> dm.state + >>> rm.state 0 - -and delta: - -.. doctest:: - - >>> dm.delta + >>> rm.delta 0 -Both of which are initialized to 0. state is meant to model -committed state, while delta represents tentative changes within a -transaction. We change the state by calling inc: +state is meant to model committed state, while delta represents +tentative changes within a transaction. We change the state by +calling inc: .. doctest:: - >>> dm.inc() + >>> rm.inc() which updates delta: .. doctest:: - >>> dm.delta + >>> rm.delta 1 but state isn't changed until we commit the transaction: .. doctest:: - >>> dm.state + >>> rm.state 0 -To commit the changes, we use 2-phase commit. We execute the first -stage by calling prepare. We need to pass a transation. Our +To commit the changes, we use 2-phase commit. We execute the first +stage by calling ``tpc_begin``. We need to pass a transation. Our sample data managers don't really use the transactions for much, -so we'll be lazy and use strings for transactions: +so we'll be lazy and use strings for transactions. The sample +data manager updates the state when we call ``tpc_vote``, after +calling ``commit``: + .. doctest:: >>> t1 = '1' - >>> dm.prepare(t1) + >>> rm.tpc_begin(t1) + >>> rm.state, rm.delta + (0, 1) + >>> rm.commit(t1) + >>> rm.tpc_vote(t1) + >>> rm.state, rm.delta + (1, 1) -The sample data manager updates the state when we call prepare: + +Now if we call tpc_finish: .. doctest:: - >>> dm.state + >>> rm.tpc_finish(t1) + +Our changes are "permanent". The state reflects the changes and the +delta has been reset to 0. + +.. doctest:: + + >>> rm.state, rm.delta + (1, 0) + + +The ``tpc_begin`` Method +======================== + +Called by the transaction manager to ask the RM to prepare to commit data. + +.. doctest:: + + >>> rm = DataManager() + >>> rm.inc() + >>> t1 = '1' + >>> rm.tpc_begin(t1) + >>> rm.tpc_vote(t1) + >>> rm.tpc_finish(t1) + >>> rm.state 1 - >>> dm.delta + >>> rm.inc() + >>> t2 = '2' + >>> rm.tpc_begin(t2) + >>> rm.tpc_vote(t2) + >>> rm.tpc_abort(t2) + >>> rm.state 1 -This is mainly so we can detect some affect of calling the methods. - -Now if we call commit: +It is an error to call tpc_begin more than once without completing +two-phase commit: .. doctest:: - >>> dm.commit(t1) + >>> rm.tpc_begin(t1) -Our changes are"permanent". The state reflects the changes and the -delta has been reset to 0. + >>> rm.tpc_begin(t1) + Traceback (most recent call last): + ... + ValueError: txn in state 'tpc_begin' but expected one of (None,) + >>> rm.tpc_abort(t1) + +If there was a preceeding savepoint, the transaction must match: .. doctest:: - >>> dm.state - 1 - >>> dm.delta - 0 + >>> rollback = rm.savepoint(t1) + >>> rm.tpc_begin(t2) + Traceback (most recent call last): + ... + TypeError: ('Transaction missmatch', '2', '1') + + >>> rm.tpc_begin(t1) + + +The ``tpc_vote`` Method +======================= -The :meth:`prepare` Method ----------------------------- +Verify that a data manager can commit the transaction. -Prepare to commit data +This is the last chance for a data manager to vote 'no'. A +data manager votes 'no' by raising an exception. + +Passed *transaction*, which is the `ITransaction` instance associated +with the transaction being committed. + + +The ``tpc_finish`` Method +========================= + +Complete two-phase commit .. doctest:: - >>> dm = DataManager() - >>> dm.inc() + >>> rm = DataManager() + >>> rm.state + 0 + >>> rm.inc() + + We start two-phase commit by calling ``tpc_begin``, ``commit``, and ``tpc_vote``: + >>> t1 = '1' - >>> dm.prepare(t1) - >>> dm.commit(t1) - >>> dm.state - 1 - >>> dm.inc() - >>> t2 = '2' - >>> dm.prepare(t2) - >>> dm.abort(t2) - >>> dm.state + >>> rm.tpc_begin(t1) + >>> rm.commit(t1) + >>> rm.tpc_vote(t1) + + We complete it by calling tpc_finish: + + >>> rm.tpc_finish(t1) + >>> rm.state 1 -It is en error to call prepare more than once without an intervening -commit or abort: +It is an error ro call tpc_finish without calling tpc_vote: .. doctest:: - >>> dm.prepare(t1) + >>> rm.inc() + >>> t2 = '2' + >>> rm.tpc_begin(t2) + >>> rm.tpc_finish(t2) + Traceback (most recent call last): + ... + ValueError: txn in state 'tpc_begin' but expected one of ('tpc_vote',) + + >>> rm.tpc_abort(t2) # clean slate + + >>> rm.tpc_begin(t2) + >>> rm.tpc_vote(t2) + >>> rm.tpc_finish(t2) + +Of course, the transactions given to tpc_begin and tpc_finish must +be the same: - >>> dm.prepare(t1) +.. doctest:: + + >>> rm.inc() + >>> t3 = '3' + >>> rm.tpc_begin(t3) + >>> rm.tpc_vote(t3) + >>> rm.tpc_finish(t2) Traceback (most recent call last): ... - TypeError: Already prepared + TypeError: ('Transaction missmatch', '2', '3') + - >>> dm.prepare(t2) +The ``tpc_abort`` Method +======================== + +Abort a transaction during two-phase commit *after* ``tpc_vote`` has +been called. + +Here, we will ignore the fact that this is only called after +``tpc_vote`` and simulate that using ``inc``. + +.. doctest:: + + >>> rm = DataManager() + >>> rm.inc() + >>> rm.state, rm.delta + (0, 1) + >>> t1 = '1' + >>> rm.tpc_abort(t1) + >>> rm.state, rm.delta + (0, 0) + +The abort method also throws away work done in savepoints: + +.. doctest:: + + >>> rm.inc() + >>> r = rm.savepoint(t1) + >>> rm.inc() + >>> r = rm.savepoint(t1) + >>> rm.state, rm.delta + (0, 2) + >>> rm.tpc_abort(t1) + >>> rm.state, rm.delta + (0, 0) + +If savepoints are used, abort must be passed the same +transaction: + +.. doctest:: + + >>> rm.inc() + >>> r = rm.savepoint(t1) + >>> t2 = '2' + >>> rm.tpc_abort(t2) Traceback (most recent call last): ... - TypeError: Already prepared + TypeError: ('Transaction missmatch', '2', '1') - >>> dm.abort(t1) + >>> rm.tpc_abort(t1) -If there was a preceeding savepoint, the transaction must match: +The abort method is also used to abort a two-phase commit: .. doctest:: - >>> rollback = dm.savepoint(t1) - >>> dm.prepare(t2) + >>> rm.inc() + >>> rm.state, rm.delta + (0, 1) + >>> rm.tpc_begin(t1) + >>> rm.state, rm.delta + (0, 1) + >>> rm.tpc_vote(t1) + >>> rm.state, rm.delta + (1, 1) + >>> rm.tpc_abort(t1) + >>> rm.state, rm.delta + (0, 0) + +Of course, the transactions passed to prepare and abort must +match: + +.. doctest:: + + >>> rm.tpc_begin(t1) + >>> rm.tpc_abort(t2) Traceback (most recent call last): - ,,, + ... TypeError: ('Transaction missmatch', '2', '1') - >>> dm.prepare(t1) + >>> rm.tpc_abort(t1) -The :meth:`abort` method --------------------------- +This should never fail. + +The ``abort`` method +==================== The abort method can be called before two-phase commit to throw away work done in the transaction: @@ -193,26 +336,13 @@ >>> dm.abort(t1) -The abort method is also used to abort a two-phase commit: - -.. doctest:: - - >>> dm.inc() - >>> dm.state, dm.delta - (0, 1) - >>> dm.prepare(t1) - >>> dm.state, dm.delta - (1, 1) - >>> dm.abort(t1) - >>> dm.state, dm.delta - (0, 0) - -Of course, the transactions passed to prepare and abort must -match: +Of course, the transactions passed to abort must +match. (Since it's called before ``tpc_vote`` is called, +there might be no current transaction.) .. doctest:: - >>> dm.prepare(t1) + >>> dm.tpc_begin(t1) >>> dm.abort(t2) Traceback (most recent call last): ... @@ -221,11 +351,11 @@ >>> dm.abort(t1) +The ``commit`` method +===================== -The :meth:`commit` method ---------------------------- - -Called to omplete two-phase commit +Called after ``tpc_begin`` to make changes persistent and prepare for +voting. .. doctest:: @@ -234,51 +364,50 @@ 0 >>> dm.inc() -We start two-phase commit by calling prepare: +We start two-phase commit by calling ``tpc_begin`` .. doctest:: >>> t1 = '1' - >>> dm.prepare(t1) + >>> dm.tpc_begin(t1) - We complete it by calling commit: + We complete it by calling ``commit``, ``tpc_vote``, and ``tpc_finish``: .. doctest:: >>> dm.commit(t1) + >>> dm.tpc_vote(t1) + >>> dm.tpc_finish(t1) >>> dm.state 1 -It is an error ro call commit without calling prepare first: +It is an error to call commit without calling ``tpc_begin`` first: .. doctest:: - >>> dm.inc() + >>> dm = DataManager() >>> t2 = '2' >>> dm.commit(t2) Traceback (most recent call last): ... TypeError: Not prepared to commit - >>> dm.prepare(t2) - >>> dm.commit(t2) - -If course, the transactions given to prepare and commit must +If course, the transactions given to ``tpc_begin`` and commit must be the same: .. doctest:: - >>> dm.inc() + >>> dm = DataManager() >>> t3 = '3' - >>> dm.prepare(t3) + >>> dm.tpc_begin(t3) >>> dm.commit(t2) Traceback (most recent call last): ... TypeError: ('Transaction missmatch', '2', '3') -The :meth:`savepoint` method ------------------------------- +The ``savepoint`` Method +======================== Provide the ability to rollback transaction state @@ -302,42 +431,43 @@ .. doctest:: - >>> dm = DataManager() - >>> dm.inc() + >>> rm = DataManager() + >>> rm.inc() >>> t1 = '1' - >>> r = dm.savepoint(t1) - >>> dm.state, dm.delta + >>> r = rm.savepoint(t1) + >>> rm.state, rm.delta (0, 1) - >>> dm.inc() - >>> dm.state, dm.delta + >>> rm.inc() + >>> rm.state, rm.delta (0, 2) >>> r.rollback() - >>> dm.state, dm.delta + >>> rm.state, rm.delta (0, 1) - >>> dm.prepare(t1) - >>> dm.commit(t1) - >>> dm.state, dm.delta + >>> rm.tpc_begin(t1) + >>> rm.tpc_vote(t1) + >>> rm.tpc_finish(t1) + >>> rm.state, rm.delta (1, 0) Savepoints must have the same transaction: .. doctest:: - >>> r1 = dm.savepoint(t1) - >>> dm.state, dm.delta + >>> r1 = rm.savepoint(t1) + >>> rm.state, rm.delta (1, 0) - >>> dm.inc() - >>> dm.state, dm.delta + >>> rm.inc() + >>> rm.state, rm.delta (1, 1) >>> t2 = '2' - >>> r2 = dm.savepoint(t2) + >>> r2 = rm.savepoint(t2) Traceback (most recent call last): ... TypeError: ('Transaction missmatch', '2', '1') - >>> r2 = dm.savepoint(t1) - >>> dm.inc() - >>> dm.state, dm.delta + >>> r2 = rm.savepoint(t1) + >>> rm.inc() + >>> rm.state, rm.delta (1, 2) If we rollback to an earlier savepoint, we discard all work @@ -346,7 +476,7 @@ .. doctest:: >>> r1.rollback() - >>> dm.state, dm.delta + >>> rm.state, rm.delta (1, 0) and we can no longer rollback to the later savepoint: @@ -365,16 +495,16 @@ >>> r1.rollback() >>> r1.rollback() >>> r1.rollback() - >>> dm.state, dm.delta + >>> rm.state, rm.delta (1, 0) - >>> dm.inc() - >>> dm.inc() - >>> dm.inc() - >>> dm.state, dm.delta + >>> rm.inc() + >>> rm.inc() + >>> rm.inc() + >>> rm.state, rm.delta (1, 3) >>> r1.rollback() - >>> dm.state, dm.delta + >>> rm.state, rm.delta (1, 0) But we can't rollback to a savepoint after it has been @@ -382,8 +512,9 @@ .. doctest:: - >>> dm.prepare(t1) - >>> dm.commit(t1) + >>> rm.tpc_begin(t1) + >>> rm.tpc_vote(t1) + >>> rm.tpc_finish(t1) >>> r1.rollback() Traceback (most recent call last): diff -Nru transaction-2.4.0/docs/developer.rst transaction-3.0.0/docs/developer.rst --- transaction-2.4.0/docs/developer.rst 2018-10-23 14:27:09.000000000 +0000 +++ transaction-3.0.0/docs/developer.rst 2019-12-11 16:24:39.000000000 +0000 @@ -1,30 +1,21 @@ -:mod:`transaction` Developer Documentation -========================================== - -Transaction objects manage resources for an individual activity. - -Compatibility issues --------------------- - -The implementation of Transaction objects involves two layers of -backwards compatibility, because this version of transaction supports -both ZODB 3 and ZODB 4. Zope is evolving towards the ZODB4 -interfaces. - -Transaction has two methods for a resource manager to call to -participate in a transaction -- register() and join(). join() takes a -resource manager and adds it to the list of resources. register() is -for backwards compatibility. It takes a persistent object and -registers its _p_jar attribute. TODO: explain adapter +================================= + ``transaction`` Developer Notes +================================= + +.. currentmodule:: transaction.interfaces + +Transaction objects manage resources for an individual activity. This +document contains some notes that will help in understanding how +transactions work, and how to use them to accomplish specific objectives. Two-phase commit ----------------- +================ A transaction commit involves an interaction between the transaction -object and one or more resource managers. The transaction manager +object and one or more resource managers. The transaction manager calls the following four methods on each resource manager; it calls -tpc_begin() on each resource manager before calling commit() on any of -them. +`IDataManager.tpc_begin` on each resource manager before calling +`IDataManager.commit` on any of them. 1. tpc_begin(txn) 2. commit(txn) @@ -32,62 +23,70 @@ 4. tpc_finish(txn) Before-commit hook ------------------- +================== -Sometimes, applications want to execute some code when a transaction is -committed. For example, one might want to delay object indexing until a -transaction commits, rather than indexing every time an object is changed. -Or someone might want to check invariants only after a set of operations. A -pre-commit hook is available for such use cases: use addBeforeCommitHook(), -passing it a callable and arguments. The callable will be called with its -arguments at the start of the commit (but not for substransaction commits). +Sometimes, applications want to execute some code when a transaction +is committed. For example, one might want to delay object indexing +until a transaction commits, rather than indexing every time an object +is changed. Or someone might want to check invariants only after a set +of operations. A pre-commit hook is available for such use cases: use +`ITransaction.addBeforeCommitHook`, passing it a callable and +arguments. The callable will be called with its arguments at the start +of the commit. After-commit hook ------------------- +================= + +Sometimes, applications want to execute code after a transaction +commit attempt succeeds or aborts. For example, one might want to +launch non transactional code after a successful commit. Or still +someone might want to launch asynchronous code after. A post-commit +hook is available for such use cases: use +`ITransaction.addAfterCommitHook`, passing it a callable and +arguments. The callable will be called with a Boolean value +representing the status of the commit operation as first argument +(true if successfull or false iff aborted) preceding its arguments at +the start of the commit. + +Abort hooks +=========== -Sometimes, applications want to execute code after a transaction commit -attempt succeeds or aborts. For example, one might want to launch non -transactional code after a successful commit. Or still someone might -want to launch asynchronous code after. A post-commit hook is -available for such use cases: use addAfterCommitHook(), passing it a -callable and arguments. The callable will be called with a Boolean -value representing the status of the commit operation as first -argument (true if successfull or false iff aborted) preceding its -arguments at the start of the commit (but not for substransaction -commits). Commit hooks are not called for transaction.abort(). +Commit hooks are not called for `ITransaction.abort`. For that, use +`ITransaction.addBeforeAbortHook` or `ITransaction.addAfterAbortHook`. Error handling --------------- +============== When errors occur during two-phase commit, the transaction manager -aborts all the resource managers. The specific methods it calls -depend on whether the error occurs before or after the call to -tpc_vote() on that transaction manager. - -If the resource manager has not voted, then the resource manager will -have one or more uncommitted objects. There are two cases that lead -to this state; either the transaction manager has not called commit() -for any objects on this resource manager or the call that failed was a -commit() for one of the objects of this resource manager. For each -uncommitted object, including the object that failed in its commit(), -call abort(). - -Once uncommitted objects are aborted, tpc_abort() or abort_sub() is -called on each resource manager. - -Synchronization ---------------- - -You can register sychronization objects (synchronizers) with the -tranasction manager. The synchronizer must implement -beforeCompletion() and afterCompletion() methods. The transaction -manager calls beforeCompletion() when it starts a top-level two-phase -commit. It calls afterCompletion() when a top-level transaction is -committed or aborted. The methods are passed the current Transaction -as their only argument. +aborts all joined the data managers. The specific methods it calls depend +on whether the error occurs before or after any call to `IDataManager.tpc_vote` +joined to that transaction. + +If a data manager has not voted, then the data manager will have one +or more uncommitted objects. There are two cases that lead to this +state; either the transaction manager has not called +`IDataManager.commit` for any joined data managers, or the call that +failed was a `IDataManager.commit` for one of the joined data +managers. For each uncommitted data manager, including the object that +failed in its ``commit()``, `IDataManager.abort` is called. + +Once uncommitted objects are aborted, `IDataManager.tpc_abort` is +called on each data manager. + +Transaction Manager Lifecycle Notifications (Synchronization) +============================================================= + +You can register sychronization objects (`synchronizers +`) with the tranasction manager. The synchronizer must +implement `ISynchronizer.beforeCompletion` and +`ISynchronizer.afterCompletion` methods. The transaction manager calls +``beforeCompletion`` when it starts a top-level two-phase commit. It +calls ``afterCompletion`` when a top-level transaction is committed or +aborted. The methods are passed the current `ITransaction` as their only +argument. Explicit vs implicit transactions ---------------------------------- +================================= By default, transactions are implicitly managed. Calling ``begin()`` on a transaction manager implicitly aborts the previous transaction diff -Nru transaction-2.4.0/docs/doom.rst transaction-3.0.0/docs/doom.rst --- transaction-2.4.0/docs/doom.rst 2018-10-23 14:27:09.000000000 +0000 +++ transaction-3.0.0/docs/doom.rst 2019-12-11 16:24:39.000000000 +0000 @@ -43,7 +43,7 @@ ... count += access_count ... return count ... def sortKey(self): - ... return 1 + ... return '1' Start a new transaction: diff -Nru transaction-2.4.0/docs/hooks.rst transaction-3.0.0/docs/hooks.rst --- transaction-2.4.0/docs/hooks.rst 2018-10-23 14:27:09.000000000 +0000 +++ transaction-3.0.0/docs/hooks.rst 2019-12-11 16:24:39.000000000 +0000 @@ -377,11 +377,10 @@ .. doctest:: - >>> mgr = TransactionManager() - >>> do = DataObject(mgr) - >>> t = begin() - >>> t._manager._txn is not None + >>> t._manager is not None + True + >>> t._manager._txn is t True >>> t.addAfterCommitHook(hook, ('-', 1)) @@ -390,7 +389,9 @@ >>> log ["True arg '-' kw1 1 kw2 'no_kw2'"] - >>> t._manager._txn is not None - False + >>> t._manager is None + True + >>> mgr._txn is None + True >>> reset_log() diff -Nru transaction-2.4.0/docs/index.rst transaction-3.0.0/docs/index.rst --- transaction-2.4.0/docs/index.rst 2018-10-23 14:27:09.000000000 +0000 +++ transaction-3.0.0/docs/index.rst 2019-12-11 16:24:39.000000000 +0000 @@ -1,5 +1,6 @@ -:mod:`transaction` Documentation -================================ +================================== + ``transaction`` Documentation +================================== A general transaction support library for Python. @@ -14,6 +15,23 @@ the code in a web request to run inside of a transaction, and aborts the transaction automatically if an error occurs. It's also not difficult to create your own backends if necessary. +.. rubric:: Additional Documentation + +.. toctree:: + :maxdepth: 1 + + changes + convenience + doom + savepoint + hooks + datamanager + integrations + sqlalchemy + api + developer + + Getting the transaction package =============================== @@ -24,6 +42,7 @@ After this, the package can be imported in your Python code, but there are a few things that we need to explain before doing that. + Using transactions ================== @@ -46,28 +65,31 @@ transaction.abort() + Things you need to know about the transaction machinery ======================================================= -Transactions ------------- +.. rubric:: Transactions -A transaction consists of one or more operations that we want to perform as a + +A consists of one or more operations that we want to perform as a single action. It's an all or nothing proposition: either all the operations that are part of the transaction are completed successfully or none of them have any effect. -In the transaction package, a transaction object represents a running -transaction that can be committed or aborted in the end. - -Transaction managers --------------------- - -Applications interact with a transaction using a transaction manager, which is -responsible for establishing the transaction boundaries. Basically this means -that it creates the transactions and keeps track of the current one. Whenever -an application wants to use the transaction machinery, it gets the current -transaction from the transaction manager before starting any operations +In the transaction package, a `transaction object ` +represents a running transaction that can be committed or aborted in +the end. + +.. rubric:: Transaction managers + +Applications interact with a transaction using a `transaction manager +`, which is responsible for establishing the +transaction boundaries. Basically this means that it creates the +transactions and keeps track of the current one. Whenever an +application wants to use the transaction machinery, it gets the +current transaction from the transaction manager before starting any +operations The default transaction manager, `transaction.manager`, is thread local. You use it as a global variable, but every thread has it's own @@ -76,28 +98,29 @@ Application developers will most likely never need to create their own transaction managers. -Data Managers -------------- +.. rubric:: Data Managers -A data manager handles the interaction between the transaction manager and the -data storage mechanism used by the application, which can be an object storage -like the ZODB, a relational database, a file or any other storage mechanism +A `data manager ` handles the +interaction between the transaction manager and the data storage +mechanism used by the application, which can be an object storage like +the ZODB, a relational database, a file or any other storage mechanism that the application needs to control. -The data manager provides a common interface for the transaction manager to use -while a transaction is running. To be part of a specific transaction, a data -manager has to 'join' it. Any number of data managers can join a transaction, -which means that you could for example perform writing operations on a ZODB -storage and a relational database as part of the same transaction. The -transaction manager will make sure that both data managers can commit the -transaction or none of them does. +The data manager provides a common interface for the transaction +manager to use while a transaction is running. To be part of a +specific transaction, a data manager has to `join +` it. Any number of data +managers can join a transaction, which means that you could for +example perform writing operations on a ZODB storage and a relational +database as part of the same transaction. The transaction manager will +make sure that both data managers can commit the transaction or none +of them does. An application developer will need to write a data manager for each different type of storage that the application uses. There are also third party data managers that can be used instead. -The two phase commit protocol ------------------------------ +.. rubric:: The two phase commit protocol The transaction machinery uses a two phase commit protocol for coordinating all participating data managers in a transaction. The two phases work like follows: @@ -115,39 +138,24 @@ The two phase commit sequence requires that all the storages being used are capable of rolling back or aborting changes. -Savepoints ----------- +.. rubric:: Savepoints -A savepoint allows a data manager to save work to its storage without -committing the full transaction. In other words, the transaction will go on, -but if a rollback is needed we can get back to this point instead of starting -all over. +A savepoint allows `supported data managers +` to save work to their +storage without committing the full transaction. In other words, the +transaction will go on, but if a rollback is needed we can get back to +this point instead of starting all over. Savepoints are also useful to free memory that would otherwise be used to keep the whole state of the transaction. This can be very important when a transaction attempts a large number of changes. -Additional Documentation -======================== - -.. toctree:: - - sqlalchemy - convenience - doom - savepoint - hooks - datamanager - resourcemanager - integrations - api - developer - .. [#wrapped] The thread-local transaction manager, - `transaction.manager` wraps a regular transaction manager. You can - get the wrapped transaction manager using the `manager` attribute. - Implementers of data managers can use this **advanced** feature to - allow graceful shutdown from a central/main thread, by having their - `close` methods call `unregisterSynch` on the wrapped transaction + `transaction.manager` wraps a regular transaction manager. You can + get the wrapped transaction manager using the ``manager`` + attribute. Implementers of data managers can use this **advanced** + feature to allow graceful shutdown from a central/main thread, by + having their ``close`` methods call + `~.ITransactionManager.unregisterSynch` on the wrapped transaction manager they obtained when created or opened. diff -Nru transaction-2.4.0/docs/integrations.rst transaction-3.0.0/docs/integrations.rst --- transaction-2.4.0/docs/integrations.rst 2018-10-23 14:27:09.000000000 +0000 +++ transaction-3.0.0/docs/integrations.rst 2019-12-11 16:24:39.000000000 +0000 @@ -1,21 +1,25 @@ -Transaction integrations / Data Manager Implementations -======================================================== +========================================================= + Transaction integrations / Data Manager Implementations +========================================================= 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 -Nru transaction-2.4.0/docs/resourcemanager.rst transaction-3.0.0/docs/resourcemanager.rst --- transaction-2.4.0/docs/resourcemanager.rst 2018-10-23 14:27:09.000000000 +0000 +++ transaction-3.0.0/docs/resourcemanager.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,399 +0,0 @@ -Writing a Resource Manager -========================== - -Simple Resource Manager ------------------------ - -.. doctest:: - - >>> from transaction.tests.examples import ResourceManager - -This :class:`transaction.tests.examples.ResourceManager` -class provides a trivial resource-manager implementation and doc -strings to illustrate the protocol and to provide a tool for writing -tests. - -Our sample resource manager has state that is updated through an inc -method and through transaction operations. - -When we create a sample resource manager: - -.. doctest:: - - >>> rm = ResourceManager() - -It has two pieces state, state and delta, both initialized to 0: - -.. doctest:: - - >>> rm.state - 0 - >>> rm.delta - 0 - -state is meant to model committed state, while delta represents -tentative changes within a transaction. We change the state by -calling inc: - -.. doctest:: - - >>> rm.inc() - -which updates delta: - -.. doctest:: - - >>> rm.delta - 1 - -but state isn't changed until we commit the transaction: - -.. doctest:: - - >>> rm.state - 0 - -To commit the changes, we use 2-phase commit. We execute the first -stage by calling prepare. We need to pass a transation. Our -sample resource managers don't really use the transactions for much, -so we'll be lazy and use strings for transactions. The sample -resource manager updates the state when we call tpc_vote: - - -.. doctest:: - - >>> t1 = '1' - >>> rm.tpc_begin(t1) - >>> rm.state, rm.delta - (0, 1) - - >>> rm.tpc_vote(t1) - >>> rm.state, rm.delta - (1, 1) - - Now if we call tpc_finish: - - >>> rm.tpc_finish(t1) - -Our changes are "permanent". The state reflects the changes and the -delta has been reset to 0. - -.. doctest:: - - >>> rm.state, rm.delta - (1, 0) - - -The :meth:`tpc_begin` Method ------------------------------ - -Called by the transaction manager to ask the RM to prepare to commit data. - -.. doctest:: - - >>> rm = ResourceManager() - >>> rm.inc() - >>> t1 = '1' - >>> rm.tpc_begin(t1) - >>> rm.tpc_vote(t1) - >>> rm.tpc_finish(t1) - >>> rm.state - 1 - >>> rm.inc() - >>> t2 = '2' - >>> rm.tpc_begin(t2) - >>> rm.tpc_vote(t2) - >>> rm.tpc_abort(t2) - >>> rm.state - 1 - -It is an error to call tpc_begin more than once without completing -two-phase commit: - -.. doctest:: - - >>> rm.tpc_begin(t1) - - >>> rm.tpc_begin(t1) - Traceback (most recent call last): - ... - ValueError: txn in state 'tpc_begin' but expected one of (None,) - >>> rm.tpc_abort(t1) - -If there was a preceeding savepoint, the transaction must match: - -.. doctest:: - - >>> rollback = rm.savepoint(t1) - >>> rm.tpc_begin(t2) - Traceback (most recent call last): - ,,, - TypeError: ('Transaction missmatch', '2', '1') - - >>> rm.tpc_begin(t1) - - -The :meth:`tpc_vote` Method ---------------------------- - -Verify that a data manager can commit the transaction. - -This is the last chance for a data manager to vote 'no'. A -data manager votes 'no' by raising an exception. - -Passed `transaction`, which is the ITransaction instance associated with the -transaction being committed. - - -The :meth:`tpc_finish` Method ------------------------------ - -Complete two-phase commit - -.. doctest:: - - >>> rm = ResourceManager() - >>> rm.state - 0 - >>> rm.inc() - - We start two-phase commit by calling prepare: - - >>> t1 = '1' - >>> rm.tpc_begin(t1) - >>> rm.tpc_vote(t1) - - We complete it by calling tpc_finish: - - >>> rm.tpc_finish(t1) - >>> rm.state - 1 - -It is an error ro call tpc_finish without calling tpc_vote: - -.. doctest:: - - >>> rm.inc() - >>> t2 = '2' - >>> rm.tpc_begin(t2) - >>> rm.tpc_finish(t2) - Traceback (most recent call last): - ... - ValueError: txn in state 'tpc_begin' but expected one of ('tpc_vote',) - - >>> rm.tpc_abort(t2) # clean slate - - >>> rm.tpc_begin(t2) - >>> rm.tpc_vote(t2) - >>> rm.tpc_finish(t2) - -Of course, the transactions given to tpc_begin and tpc_finish must -be the same: - -.. doctest:: - - >>> rm.inc() - >>> t3 = '3' - >>> rm.tpc_begin(t3) - >>> rm.tpc_vote(t3) - >>> rm.tpc_finish(t2) - Traceback (most recent call last): - ... - TypeError: ('Transaction missmatch', '2', '3') - - -The :meth:`tpc_abort` Method ------------------------------ - -Abort a transaction - -The abort method can be called before two-phase commit to -throw away work done in the transaction: - -.. doctest:: - - >>> rm = ResourceManager() - >>> rm.inc() - >>> rm.state, rm.delta - (0, 1) - >>> t1 = '1' - >>> rm.tpc_abort(t1) - >>> rm.state, rm.delta - (0, 0) - -The abort method also throws away work done in savepoints: - -.. doctest:: - - >>> rm.inc() - >>> r = rm.savepoint(t1) - >>> rm.inc() - >>> r = rm.savepoint(t1) - >>> rm.state, rm.delta - (0, 2) - >>> rm.tpc_abort(t1) - >>> rm.state, rm.delta - (0, 0) - -If savepoints are used, abort must be passed the same -transaction: - -.. doctest:: - - >>> rm.inc() - >>> r = rm.savepoint(t1) - >>> t2 = '2' - >>> rm.tpc_abort(t2) - Traceback (most recent call last): - ... - TypeError: ('Transaction missmatch', '2', '1') - - >>> rm.tpc_abort(t1) - -The abort method is also used to abort a two-phase commit: - -.. doctest:: - - >>> rm.inc() - >>> rm.state, rm.delta - (0, 1) - >>> rm.tpc_begin(t1) - >>> rm.state, rm.delta - (0, 1) - >>> rm.tpc_vote(t1) - >>> rm.state, rm.delta - (1, 1) - >>> rm.tpc_abort(t1) - >>> rm.state, rm.delta - (0, 0) - -Of course, the transactions passed to prepare and abort must -match: - -.. doctest:: - - >>> rm.tpc_begin(t1) - >>> rm.tpc_abort(t2) - Traceback (most recent call last): - ... - TypeError: ('Transaction missmatch', '2', '1') - - >>> rm.tpc_abort(t1) - -This should never fail. - - -The :meth:`savepoint` Method ----------------------------- - -Provide the ability to rollback transaction state - -Savepoints provide a way to: - - - Save partial transaction work. For some resource managers, this - could allow resources to be used more efficiently. - - - Provide the ability to revert state to a point in a - transaction without aborting the entire transaction. In - other words, savepoints support partial aborts. - -Savepoints don't use two-phase commit. If there are errors in -setting or rolling back to savepoints, the application should -abort the containing transaction. This is *not* the -responsibility of the resource manager. - -Savepoints are always associated with a transaction. Any work -done in a savepoint's transaction is tentative until the -transaction is committed using two-phase commit. - -.. doctest:: - - >>> rm = ResourceManager() - >>> rm.inc() - >>> t1 = '1' - >>> r = rm.savepoint(t1) - >>> rm.state, rm.delta - (0, 1) - >>> rm.inc() - >>> rm.state, rm.delta - (0, 2) - >>> r.rollback() - >>> rm.state, rm.delta - (0, 1) - >>> rm.tpc_begin(t1) - >>> rm.tpc_vote(t1) - >>> rm.tpc_finish(t1) - >>> rm.state, rm.delta - (1, 0) - -Savepoints must have the same transaction: - -.. doctest:: - - >>> r1 = rm.savepoint(t1) - >>> rm.state, rm.delta - (1, 0) - >>> rm.inc() - >>> rm.state, rm.delta - (1, 1) - >>> t2 = '2' - >>> r2 = rm.savepoint(t2) - Traceback (most recent call last): - ... - TypeError: ('Transaction missmatch', '2', '1') - - >>> r2 = rm.savepoint(t1) - >>> rm.inc() - >>> rm.state, rm.delta - (1, 2) - -If we rollback to an earlier savepoint, we discard all work -done later: - -.. doctest:: - - >>> r1.rollback() - >>> rm.state, rm.delta - (1, 0) - -and we can no longer rollback to the later savepoint: - -.. doctest:: - - >>> r2.rollback() - Traceback (most recent call last): - ... - TypeError: ('Attempt to roll back to invalid save point', 3, 2) - -We can roll back to a savepoint as often as we like: - -.. doctest:: - - >>> r1.rollback() - >>> r1.rollback() - >>> r1.rollback() - >>> rm.state, rm.delta - (1, 0) - - >>> rm.inc() - >>> rm.inc() - >>> rm.inc() - >>> rm.state, rm.delta - (1, 3) - >>> r1.rollback() - >>> rm.state, rm.delta - (1, 0) - -But we can't rollback to a savepoint after it has been -committed: - -.. doctest:: - - >>> rm.tpc_begin(t1) - >>> rm.tpc_vote(t1) - >>> rm.tpc_finish(t1) - - >>> r1.rollback() - Traceback (most recent call last): - ... - TypeError: Attempt to rollback stale rollback diff -Nru transaction-2.4.0/PKG-INFO transaction-3.0.0/PKG-INFO --- transaction-2.4.0/PKG-INFO 2018-10-23 14:27:10.000000000 +0000 +++ transaction-3.0.0/PKG-INFO 2019-12-11 16:24:39.988990300 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: transaction -Version: 2.4.0 +Version: 3.0.0 Summary: Transaction management for Python Home-page: https://github.com/zopefoundation/transaction Author: Zope Corporation @@ -37,6 +37,42 @@ Changes ========= + 3.0.0 (2019-12-11) + ================== + + - Drop support for Python 3.4. + + - 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*) + (`#58 `_). + + It aborts the transaction now for all exceptions raised by *func* - even + 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 `_). + + - Make Transaction drop references to its hooks, manager, + synchronizers and data after a successful ``commit()`` and after + *any* ``abort()``. This helps avoid potential cyclic references. See + `issue 82 `_. + + - Allow synchronizers to access ``Transaction.data()`` when their + ``afterCompletion`` method is called while aborting a transaction. + + - Make it safe to call ``Transaction.abort()`` more than once. The + second and subsequent calls are no-ops. Previously a + ``ValueError(Foreign transaction)`` would be raised. + 2.4.0 (2018-10-23) ================== @@ -357,7 +393,7 @@ 1.0a1 (2007-12-18) ================== - = Initial release, branched from ZODB trunk on 2007-11-08 (aka + - Initial release, branched from ZODB trunk on 2007-11-08 (aka "3.9.0dev"). - Remove (deprecated) support for beforeCommitHook alias to @@ -380,13 +416,13 @@ Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Framework :: ZODB -Provides-Extra: testing -Provides-Extra: test Provides-Extra: docs +Provides-Extra: test +Provides-Extra: testing diff -Nru transaction-2.4.0/setup.py transaction-3.0.0/setup.py --- transaction-2.4.0/setup.py 2018-10-23 14:27:09.000000000 +0000 +++ transaction-3.0.0/setup.py 2019-12-11 16:24:39.000000000 +0000 @@ -14,7 +14,7 @@ import os from setuptools import setup, find_packages -version = '2.4.0' +version = '3.0.0' here = os.path.abspath(os.path.dirname(__file__)) @@ -43,10 +43,10 @@ "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Framework :: ZODB", diff -Nru transaction-2.4.0/tox.ini transaction-3.0.0/tox.ini --- transaction-2.4.0/tox.ini 2018-10-23 14:27:09.000000000 +0000 +++ transaction-3.0.0/tox.ini 2019-12-11 16:24:39.000000000 +0000 @@ -1,6 +1,6 @@ [tox] envlist = - py27,pypy,py34,py35,py36,py37,pypy3,coverage,docs + py27,pypy,py35,py36,py37,py38,pypy3,coverage,docs [testenv] commands = diff -Nru transaction-2.4.0/transaction/_compat.py transaction-3.0.0/transaction/_compat.py --- transaction-2.4.0/transaction/_compat.py 2018-10-23 14:27:09.000000000 +0000 +++ transaction-3.0.0/transaction/_compat.py 2019-12-11 16:24:39.000000000 +0000 @@ -4,23 +4,18 @@ 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): - 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 +26,7 @@ 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 +38,7 @@ 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 +62,7 @@ """) -try: +try: # pragma: no cover from threading import get_ident as get_thread_ident except ImportError: # pragma: no cover # py2 diff -Nru transaction-2.4.0/transaction/__init__.py transaction-3.0.0/transaction/__init__.py --- transaction-2.4.0/transaction/__init__.py 2018-10-23 14:27:09.000000000 +0000 +++ transaction-3.0.0/transaction/__init__.py 2019-12-11 16:24:39.000000000 +0000 @@ -11,13 +11,17 @@ # FOR A PARTICULAR PURPOSE. # ############################################################################ -"""Exported transaction functions. +"""``transaction`` module: Exported transaction functions. -$Id$ """ +#: Default implementation of `~ITransaction` from transaction._transaction import Transaction +#: Default implementation of `~ISavepoint` +from transaction._transaction import Savepoint +#: A single-threaded `~ITransactionManager` from transaction._manager import TransactionManager +#: A thread-safe `~ITransactionManager` from transaction._manager import ThreadTransactionManager # NB: "with transaction:" does not work under Python 3 because they worked @@ -25,13 +29,23 @@ # via getattr and getattribute; see http://bugs.python.org/issue12022. On # Python 3, you must use ``with transaction.manager`` instead. +#: The default transaction manager (a `~.ThreadTransactionManager`). All other functions in +#: this module refer to this object. manager = ThreadTransactionManager() +#: See `.ITransactionManager.get` get = __enter__ = manager.get +#: See `.ITransactionManager.begin` begin = manager.begin +#: See `.ITransactionManager.commit` commit = manager.commit +#: See `.ITransactionManager.abort` abort = manager.abort __exit__ = manager.__exit__ +#: See `.ITransactionManager.doom` doom = manager.doom +#: See `.ITransactionManager.isDoomed` isDoomed = manager.isDoomed +#: See `.ITransactionManager.savepoint` savepoint = manager.savepoint +#: See `.ITransactionManager.attempts` attempts = manager.attempts diff -Nru transaction-2.4.0/transaction/interfaces.py transaction-3.0.0/transaction/interfaces.py --- transaction-2.4.0/transaction/interfaces.py 2018-10-23 14:27:09.000000000 +0000 +++ transaction-3.0.0/transaction/interfaces.py 2019-12-11 16:24:39.000000000 +0000 @@ -19,6 +19,11 @@ """An object that manages a sequence of transactions. Applications use transaction managers to establish transaction boundaries. + + A transaction manager supports the "context manager" protocol: + Its `__enter__` begins a new transaction; its `__exit__` commits + the current transaction if no exception has occured; otherwise, + it aborts it. """ explicit = Attribute( @@ -26,8 +31,10 @@ This is true if the transaction manager is in explicit mode. In explicit mode, transactions must be begun explicitly, by - calling ``begin()`` and ended explicitly by calling - ``commit()`` or ``abort()``. + calling `begin` and ended explicitly by calling + `commit` or `abort`. + + .. versionadded:: 2.1.0 """) @@ -38,53 +45,53 @@ manager not in explicit mode, the previous transaction will be aborted. If an existing transaction is in progress and the transaction manager is in explicit mode, an - ``AlreadyInTransaction`` exception will be raised.. + `AlreadyInTransaction` exception will be raised.. - The ``newTransaction`` method of registered synchronizers is called, + The `~ISynchronizer.newTransaction` method of registered synchronizers is called, passing the new transaction object. Note that when not in explicit mode, transactions may be - started implicitly without calling ``begin``. In that case, + started implicitly without calling `begin`. In that case, ``newTransaction`` isn't called because the transaction manager doesn't know when to call it. The transaction is likely to have begun long before the transaction manager is - involved. (Conceivably the ``commit`` and ``abort`` methods - could call ``begin``, but they don't.) + involved. (Conceivably the `commit` and `abort` methods + could call `begin`, but they don't.) """ def get(): """Get the current transaction. In explicit mode, if a transaction hasn't begun, a - ``NoTransaction`` exception will be raised. + `NoTransaction` exception will be raised. """ def commit(): """Commit the current transaction. In explicit mode, if a transaction hasn't begun, a - ``NoTransaction`` exception will be raised. + `NoTransaction` exception will be raised. """ def abort(): """Abort the current transaction. In explicit mode, if a transaction hasn't begun, a - ``NoTransaction`` exception will be raised. + `NoTransaction` exception will be raised. """ def doom(): """Doom the current transaction. In explicit mode, if a transaction hasn't begun, a - ``NoTransaction`` exception will be raised. + `NoTransaction` exception will be raised. """ def isDoomed(): """Returns True if the current transaction is doomed, otherwise False. In explicit mode, if a transaction hasn't begun, a - ``NoTransaction`` exception will be raised. + `NoTransaction` exception will be raised. """ def savepoint(optimistic=False): @@ -94,58 +101,90 @@ don't support savepoints can be used, but an error will be raised if the savepoint is rolled back. - An ISavepoint object is returned. + An `ISavepoint` object is returned. In explicit mode, if a transaction hasn't begun, a - ``NoTransaction`` exception will be raised. + `NoTransaction` exception will be raised. """ def registerSynch(synch): - """Register an ISynchronizer. + """Register an `ISynchronizer`. Synchronizers are notified about some major events in a transaction's - life. See ISynchronizer for details. + life. See `ISynchronizer` for details. If a synchronizer registers while there is an active - transaction, its newTransaction method will be called with the + transaction, its ``newTransaction`` method will be called with the active transaction. """ def unregisterSynch(synch): - """Unregister an ISynchronizer. + """Unregister an `ISynchronizer`. Synchronizers are notified about some major events in a transaction's - life. See ISynchronizer for details. + life. See `ISynchronizer` for details. """ def clearSynchs(): - """Unregister all registered ISynchronizers. + """Unregister all registered `ISynchronizer` objects. This exists to support test cleanup/initialization """ def registeredSynchs(): - """Determine if any ISynchronizers are registered. + """Determine if any `ISynchronizers` are registered. Return true if any are registered, and return False otherwise. This exists to support test cleanup/initialization """ + def attempts(number=3): + """Generate up to *number* (transactional) context managers. + + This method is typically used as follows:: + + for attempt in transaction_manager.attempts(): + with attempt: + *with block* + + The ``with attempt:`` starts a new transaction for the + execution of the *with block*. If the execution succeeds, the + (then current) transaction is commited and the ``for`` loop + terminates. If the execution raised an exception, then the + transaction is aborted. If the exception was some kind of + `retriable error ` and the + maximal number of attempts is not yet reached, then a next + iteration of the ``for`` loop starts. In all other cases, + the ``for`` loop terminates with the exception. + """ + + def run(func=None, tries=3): + """Call *func()* in its own transaction; retry + in case of some kind of `retriable error `. + + The call is tried up to *tries* times. + + The call is performed in a new transaction. After the call, + the (then current) transaction is committed (no exception) or + aborted (exception). + + `run` supports the alternative signature ``run(tries=3)``. If + *func* is not given or passed as `None`, then the call to + `run` returns a function taking *func* as argument and then + calling ``run(func, tries)``. + """ + + class ITransaction(Interface): """Object representing a running transaction. - - Objects with this interface may represent different transactions - during their lifetime (.begin() can be called to start a new - transaction using the same instance, although that example is - deprecated and will go away in ZODB 3.6). """ user = Attribute( """A user name associated with the transaction. The format of the user name is defined by the application. The value - is text (unicode). Storages record the user value, as meta-data, + is text (unicode). Storages record the user value, as meta-data, when a transaction commits. A storage may impose a limit on the size of the value; behavior is @@ -156,8 +195,8 @@ description = Attribute( """A textual description of the transaction. - The value is text (unicode). Method note() is the intended - way to set the value. Storages record the description, as meta-data, + The value is text (unicode). Method `note` is the intended + way to set the value. Storages record the description, as meta-data, when a transaction commits. A storage may impose a limit on the size of the description; behavior @@ -172,7 +211,7 @@ """Finalize the transaction. This executes the two-phase commit algorithm for all - IDataManager objects associated with the transaction. + `IDataManager` objects associated with the transaction. """ def abort(): @@ -186,7 +225,7 @@ """Doom the transaction. Dooms the current transaction. This will cause - DoomedTransactionException to be raised on any attempt to commit the + `DoomedTransaction` to be raised on any attempt to commit the transaction. Otherwise the transaction will behave as if it was active. @@ -195,38 +234,38 @@ def savepoint(optimistic=False): """Create a savepoint. - If the optimistic argument is true, then data managers that don't + If the *optimistic* argument is true, then data managers that don't support savepoints can be used, but an error will be raised if the savepoint is rolled back. - An ISavepoint object is returned. + An `ISavepoint` object is returned. """ def join(datamanager): """Add a data manager to the transaction. - `datamanager` must provide the transactions.interfaces.IDataManager + *datamanager* must provide the `IDataManager` interface. """ def note(text): """Add text (unicode) to the transaction description. - This modifies the `.description` attribute; see its docs for more - detail. First surrounding whitespace is stripped from `text`. If - `.description` is currently an empty string, then the stripped text + This modifies the `description` attribute; see its docs for more + detail. First surrounding whitespace is stripped from *text*. If + `description` is currently an empty string, then the stripped text becomes its value, else two newlines and the stripped text are - appended to `.description`. + appended to `description`. """ 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 - must be picklable and json serializable (not an instance). + :param value: + must be picklable and json serializable Multiple calls may be made to set multiple extension properties, provided the names are distinct. @@ -240,81 +279,131 @@ """ def addBeforeCommitHook(hook, args=(), kws=None): - """Register a hook to call before the transaction is committed. + """Register a hook to call before the transaction is + committed. - The specified hook function will be called after the transaction's - commit method has been called, but before the commit 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. - - Hooks are called only for a top-level commit. A - savepoint creation does not call any hooks. If the - transaction is aborted, hooks are not called, and are discarded. - Calling a hook "consumes" its registration too: hook registrations - do not persist across transactions. If it's desired to call the same - hook on every transaction commit, then addBeforeCommitHook() must be - called with that hook during every transaction; in such a case - consider registering a synchronizer object via a TransactionManager's - registerSynch() method instead. + The specified hook function will be called after the + transaction's commit method has been called, but before the + commit process has been started. + + :param sequence args: + Additional positional arguments to be passed to the hook. + The default is to pass no positional arguments. + :param dict kws: + Keyword arguments to pass to the hook. The default + is to pass no keyword arguments. + + 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. + + Hooks are called only for a top-level commit. A savepoint + creation does not call any hooks. If the transaction is + aborted, hooks are not called, and are discarded. Calling a + hook "consumes" its registration too: hook registrations do + not persist across transactions. If it's desired to call the + same hook on every transaction commit, then + `addBeforeCommitHook` must be called with that hook during + every transaction; in such a case consider registering a + synchronizer object via `ITransactionManager.registerSynch` + instead. """ def getBeforeCommitHooks(): - """Return iterable producing the registered addBeforeCommit hooks. + """Return iterable producing the registered `addBeforeCommitHook` hooks. - A triple (hook, args, kws) is produced for each registered hook. + 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 commit. """ def addAfterCommitHook(hook, args=(), kws=None): - """Register a hook to call after a transaction commit attempt. + """Register a hook to call after a transaction commit + attempt. - The specified hook function will be called after the transaction - commit succeeds or aborts. The first argument passed to the hook - is a Boolean value, true if the commit succeeded, or false if the - commit aborted. `args` specifies additional positional, and `kws` - keyword, arguments to pass to the hook. `args` is a sequence of - positional arguments to be passed, defaulting to an empty tuple - (only the true/false success argument is 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. - - Hooks are called only for a top-level commit. A - savepoint creation does not call any hooks. Calling a - hook "consumes" its registration: hook registrations do not - persist across transactions. If it's desired to call the same - hook on every transaction commit, then addAfterCommitHook() must be - called with that hook during every transaction; in such a case - consider registering a synchronizer object via a TransactionManager's - registerSynch() method instead. + The specified hook function will be called after the + transaction commit succeeds or aborts. The first argument + passed to the hook is a Boolean value, `True` if the commit + succeeded, or `False` if the commit aborted. + + *args* and *kws* are interpreted as for `addBeforeCommitHook` + (with the exception that there is always one positional + argument, the commit status). + + As with `addBeforeCommitHook`, multiple hooks can be + registered, savepoint creation doesn't call any hooks, and + calling a hook consumes its registration. """ def getAfterCommitHooks(): - """Return iterable producing the registered addAfterCommit hooks. + """Return iterable producing the registered `addAfterCommitHook` + 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 commit. + As with `getBeforeCommitHooks`, 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 commit. + """ + + def addBeforeAbortHook(hook, args=(), kws=None): + """Register a hook to call before the transaction is aborted. + + The specified hook function will be called after the + transaction's abort method has been called, but before the + abort process has been started. + + *args* and *kws* are interpreted as for `addBeforeCommitHook`. + As with `addBeforeCommitHook`, multiple hooks can be + registered, savepoint creation doesn't call any hooks, and + calling a hook consumes its registration. + + 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. + """ + + def getBeforeAbortHooks(): + """Return iterable producing the registered `addBeforeAbortHook` + hooks. + + As with `getBeforeCommitHooks`, 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. + + *args* and *kws* are interpreted as for `addBeforeCommitHook`. + As with `addBeforeCommitHook`, multiple hooks can be + registered, savepoint creation doesn't call any hooks, and + calling a hook consumes its registration. + + As with `addBeforeAbortHook`, these hooks are called only for + a top-level abort. See that method for more. + """ + + def getAfterAbortHooks(): + """Return iterable producing the registered `addAfterAbortHook` + hooks. + + As with `getBeforeCommitHooks`, 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 + """Hold *data* on behalf of an object For objects such as data managers or their subobjects that work with multiple transactions, it's convenient to store @@ -323,57 +412,49 @@ on behalf of the object. The object passed should be the object that needs the data, as - opposed to simple object like a string. (Internally, the id of + opposed to a simple object like a string. (Internally, the id of the object is used as the key.) """ def data(ob): """Retrieve data held on behalf of an object. - See set_data. + See `set_data`. """ def isRetryableError(error): """Determine if the error is retryable. - Return true if any joined IRetryDataManager considers the error - transient. Such errors may occur due to concurrency issues in the - underlying storage engine. - + Returns true if any joined `IRetryDataManager` considers the + error transient *or* if the error is an instance of + `TransientError`. Such errors may occur due to concurrency + issues in the underlying storage engine. """ -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. These objects may manage data for other objects, or they may manage non-object storages, such as relational databases. For example, - a ZODB.Connection. + a `ZODB.Connection.Connection`. Note that when some data is modified, that data's data manager should join a transaction so that data can be committed when the user commits the transaction. + + These objects implement the two-phase commit protocol in order to allow + multiple data managers to safely participate in a single transaction. + The methods `tpc_begin`, `commit`, `tpc_vote`, and then either + `tpc_finish` or `tpc_abort` are normally called in that order when + committing a transaction. """ transaction_manager = Attribute( """The transaction manager (TM) used by this data manager. This is a public attribute, intended for read-only use. The value - is an instance of ITransactionManager, typically set by the data + is an instance of `ITransactionManager`, typically set by the data manager's constructor. """) @@ -392,28 +473,23 @@ changes, the data manager must rejoin the transaction. """ - # Two-phase commit protocol. These methods are called by the ITransaction - # object associated with the transaction being committed. The sequence - # of calls normally follows this regular expression: - # tpc_begin commit tpc_vote (tpc_finish | tpc_abort) - def tpc_begin(transaction): """Begin commit of a transaction, starting the two-phase commit. - transaction is the ITransaction instance associated with the + *transaction* is the `ITransaction` instance associated with the transaction being committed. """ def commit(transaction): """Commit modifications to registered objects. - Save changes to be made persistent if the transaction commits (if - tpc_finish is called later). If tpc_abort is called later, changes - must not persist. - - This includes conflict detection and handling. If no conflicts or - errors occur, the data manager should be prepared to make the - changes persist when tpc_finish is called. + Save changes to be made persistent if the transaction commits + (if `tpc_finish` is called later). If `tpc_abort` is called + later, changes must not persist. + + This includes conflict detection and handling. If no conflicts + or errors occur, the data manager should be prepared to make + the changes persist when `tpc_finish` is called. """ def tpc_vote(transaction): @@ -422,7 +498,7 @@ This is the last chance for a data manager to vote 'no'. A data manager votes 'no' by raising an exception. - transaction is the ITransaction instance associated with the + *transaction* is the `ITransaction` instance associated with the transaction being committed. """ @@ -431,7 +507,7 @@ Make all changes to objects modified by this transaction persist. - transaction is the ITransaction instance associated with the + *transaction* is the `ITransaction` instance associated with the transaction being committed. This should never fail. If this raises an exception, the @@ -446,21 +522,23 @@ the data manager. Abandon all changes to objects modified by this transaction. - transaction is the ITransaction instance associated with the + *transaction* is the `ITransaction` instance associated with the transaction being committed. This should never fail. """ def sortKey(): - """Return a key to use for ordering registered DataManagers. + """Return a key to use for ordering registered + `IDataManagers`. - In order to guarantee a total ordering, keys must be strings. + In order to guarantee a total ordering, keys **must** be + `strings `. - ZODB uses a global sort order to prevent deadlock when it commits - transactions involving multiple resource managers. The resource - manager must define a sortKey() method that provides a global ordering - for resource managers. + Transactions use a global sort order to prevent deadlock when + committing transactions involving multiple data managers. + The data managers **must** define a `sortKey` method that + provides a global ordering across all registered data managers. """ # Alternate version: #"""Return a consistent sort key for this connection. @@ -473,7 +551,7 @@ class ISavepointDataManager(IDataManager): def savepoint(): - """Return a data-manager savepoint (IDataManagerSavepoint). + """Return a data-manager savepoint (`IDataManagerSavepoint`). """ class IRetryDataManager(IDataManager): @@ -483,21 +561,23 @@ A data manager can provide this method to indicate that a a transaction that raised the given error should be retried. - This method may be called by an ITransactionManager when + This method may be called by an `ITransactionManager` when considering whether to retry a failed transaction. """ class IDataManagerSavepoint(Interface): - """Savepoint for data-manager changes for use in transaction savepoints. + """Savepoint for data-manager changes for use in transaction + savepoints. - Datamanager savepoints are used by, and only by, transaction savepoints. + Datamanager savepoints are used by, and only by, transaction + savepoints. Note that data manager savepoints don't have any notion of, or - responsibility for, validity. It isn't the responsibility of - data-manager savepoints to prevent multiple rollbacks or rollbacks after - transaction termination. Preventing invalid savepoint rollback is the - responsibility of transaction rollbacks. Application code should never - use data-manager savepoints. + responsibility for, validity. It isn't the responsibility of + data-manager savepoints to prevent multiple rollbacks or rollbacks + after transaction termination. Preventing invalid savepoint + rollback is the responsibility of transaction rollbacks. + Application code should never use data-manager savepoints. """ def rollback(): @@ -511,7 +591,7 @@ def rollback(): """Rollback any work done since the savepoint. - InvalidSavepointRollbackError is raised if the savepoint isn't valid. + `InvalidSavepointRollbackError` is raised if the savepoint isn't valid. """ valid = Attribute( @@ -543,20 +623,22 @@ """Hook that is called at the start of a transaction. This hook is called when, and only when, a transaction manager's - begin() method is called explictly. + `~ITransactionManager.begin` method is called explicitly. """ class TransactionError(Exception): """An error occurred due to normal transaction processing.""" class TransactionFailedError(TransactionError): - """Cannot perform an operation on a transaction that previously failed. + """Cannot perform an operation on a transaction that previously + failed. - An attempt was made to commit a transaction, or to join a transaction, - but this transaction previously raised an exception during an attempt - to commit it. The transaction must be explicitly aborted, either by - invoking abort() on the transaction, or begin() on its transaction - manager. + An attempt was made to commit a transaction, or to join a + transaction, but this transaction previously raised an exception + during an attempt to commit it. The transaction must be explicitly + aborted by invoking `ITransaction.abort`. (If the transaction manager + is not operating in explicit mode, then `ITransactionManager.begin` + can also be used to perform an implicit abort.) """ class DoomedTransaction(TransactionError): @@ -575,12 +657,16 @@ affects an exciting transaction, but no transaction was begun. The transaction manager was in explicit mode, so a new transaction was not explicitly created. + + .. versionadded:: 2.1.0 """ class AlreadyInTransaction(TransactionError): """Attempt to create a new transaction without ending a preceding one - An application called ``begin()`` on a transaction manager in + An application called `~ITransactionManager.begin` on a transaction manager in explicit mode, without committing or aborting the previous transaction. + + .. versionadded:: 2.1.0 """ diff -Nru transaction-2.4.0/transaction/_manager.py transaction-3.0.0/transaction/_manager.py --- transaction-2.4.0/transaction/_manager.py 2018-10-23 14:27:09.000000000 +0000 +++ transaction-3.0.0/transaction/_manager.py 2019-12-11 16:24:39.000000000 +0000 @@ -18,6 +18,7 @@ """ import sys import threading +import itertools from zope.interface import implementer @@ -60,6 +61,9 @@ @implementer(ITransactionManager) class TransactionManager(object): + """ + Single-thread implementation of `~transaction.interfaces.ITransactionManager`. + """ def __init__(self, explicit=False): self.explicit = explicit @@ -67,7 +71,7 @@ self._synchs = WeakSet() def begin(self): - """ See ITransactionManager. + """ See `~transaction.interfaces.ITransactionManager`. """ if self._txn is not None: if self.explicit: @@ -80,7 +84,7 @@ __enter__ = lambda self: self.begin() def get(self): - """ See ITransactionManager. + """ See `~transaction.interfaces.ITransactionManager`. """ if self._txn is None: if self.explicit: @@ -94,44 +98,44 @@ self._txn = None def registerSynch(self, synch): - """ See ITransactionManager. + """ See `~transaction.interfaces.ITransactionManager`. """ self._synchs.add(synch) if self._txn is not None: synch.newTransaction(self._txn) def unregisterSynch(self, synch): - """ See ITransactionManager. + """ See `~transaction.interfaces.ITransactionManager`. """ self._synchs.remove(synch) def clearSynchs(self): - """ See ITransactionManager. + """ See `~transaction.interfaces.ITransactionManager`. """ self._synchs.clear() def registeredSynchs(self): - """ See ITransactionManager. + """ See `~transaction.interfaces.ITransactionManager`. """ return bool(self._synchs) def isDoomed(self): - """ See ITransactionManager. + """ See `~transaction.interfaces.ITransactionManager`. """ return self.get().isDoomed() def doom(self): - """ See ITransactionManager. + """ See `~transaction.interfaces.ITransactionManager`. """ return self.get().doom() def commit(self): - """ See ITransactionManager. + """ See `~transaction.interfaces.ITransactionManager`. """ return self.get().commit() def abort(self): - """ See ITransactionManager. + """ See `~transaction.interfaces.ITransactionManager`. """ return self.get().abort() @@ -142,7 +146,7 @@ self.abort() def savepoint(self, optimistic=False): - """ See ITransactionManager. + """ See `~transaction.interfaces.ITransactionManager`. """ return self.get().savepoint(optimistic) @@ -177,7 +181,7 @@ return lambda func: self.run(func, tries) if tries <= 0: - raise ValueError("tries must be positive") + raise ValueError("tries must be > 0") # These are ordinarily native strings, but that's # not required. A callable class could override them @@ -190,41 +194,42 @@ name = text_(name) if name else u'' doc = text_(doc) if doc else u'' - if name != u'_': + if name and name != u'_': if doc: doc = name + u'\n\n' + doc else: doc = name - for i in range(1, tries + 1): # pragma: no branch + for try_no in itertools.count(1): txn = self.begin() if doc: txn.note(doc) - try: result = func() - txn.commit() - except Exception as v: - if i == tries: - raise # that was our last chance - retry = self._retryable(v.__class__, v) - txn.abort() - if not retry: - raise - else: + self.commit() return result + except BaseException as exc: + # Note: `abort` must not be called before `_retryable` + retry = (isinstance(exc, Exception) + and try_no < tries + and self._retryable(exc.__class__, exc)) + self.abort() + if retry: + continue + else: + raise @implementer(ITransactionManager) class ThreadTransactionManager(threading.local): """ - Thread-local transaction manager. + Thread-local `transaction manager `. A thread-local transaction manager can be used as a global variable, but has a separate copy for each thread. Advanced applications can use the `manager` attribute to get a - wrapped TransactionManager to allow cross-thread calls for + wrapped `TransactionManager` to allow cross-thread calls for graceful shutdown of data managers. """ diff -Nru transaction-2.4.0/transaction/tests/examples.py transaction-3.0.0/transaction/tests/examples.py --- transaction-2.4.0/transaction/tests/examples.py 2018-10-23 14:27:09.000000000 +0000 +++ transaction-3.0.0/transaction/tests/examples.py 2019-12-11 16:24:39.000000000 +0000 @@ -15,9 +15,8 @@ """ - class DataManager(object): - """Sample data manager + """ Sample data manager. Used by the 'datamanager' chapter in the Sphinx docs. """ @@ -26,82 +25,8 @@ self.sp = 0 self.transaction = None self.delta = 0 - self.prepared = False - - def inc(self, n=1): - self.delta += n - - def prepare(self, transaction): - if self.prepared: - raise TypeError('Already prepared') - self._checkTransaction(transaction) - self.prepared = True - self.transaction = transaction - self.state += self.delta - - def _checkTransaction(self, transaction): - if (transaction is not self.transaction - and self.transaction is not None): - raise TypeError("Transaction missmatch", - transaction, self.transaction) - - def abort(self, transaction): - self._checkTransaction(transaction) - if self.transaction is not None: - self.transaction = None - - if self.prepared: - self.state -= self.delta - self.prepared = False - - self.delta = 0 - - def commit(self, transaction): - if not self.prepared: - raise TypeError('Not prepared to commit') - self._checkTransaction(transaction) - self.delta = 0 - self.transaction = None - self.prepared = False - - def savepoint(self, transaction): - if self.prepared: - raise AssertionError("Can't get savepoint during two-phase commit") - self._checkTransaction(transaction) - self.transaction = transaction - self.sp += 1 - return Rollback(self) - - -class Rollback(object): - - def __init__(self, dm): - self.dm = dm - self.sp = dm.sp - self.delta = dm.delta - self.transaction = dm.transaction - - def rollback(self): - if self.transaction is not self.dm.transaction: - raise TypeError("Attempt to rollback stale rollback") - if self.dm.sp < self.sp: - raise TypeError("Attempt to roll back to invalid save point", - self.sp, self.dm.sp) - self.dm.sp = self.sp - self.dm.delta = self.delta - - -class ResourceManager(object): - """ Sample resource manager. - - Used by the 'resourcemanager' chapter in the Sphinx docs. - """ - def __init__(self): - self.state = 0 - self.sp = 0 - self.transaction = None - self.delta = 0 self.txn_state = None + self.begun = False def _check_state(self, *ok_states): if self.txn_state not in ok_states: @@ -110,7 +35,7 @@ def _checkTransaction(self, transaction): if (transaction is not self.transaction - and self.transaction is not None): + and self.transaction is not None): raise TypeError("Transaction missmatch", transaction, self.transaction) @@ -122,6 +47,7 @@ self._check_state(None) self.transaction = transaction self.txn_state = 'tpc_begin' + self.begun = True def tpc_vote(self, transaction): self._checkTransaction(transaction) @@ -134,7 +60,6 @@ self._check_state('tpc_vote') self.delta = 0 self.transaction = None - self.prepared = False self.txn_state = None def tpc_abort(self, transaction): @@ -156,8 +81,22 @@ self.sp += 1 return SavePoint(self) - def discard(self, transaction): - "Does nothing" + def abort(self, transaction): + self._checkTransaction(transaction) + if self.transaction is not None: + self.transaction = None + + if self.begun: + self.state -= self.delta + self.begun = False + + self.delta = 0 + + def commit(self, transaction): + if not self.begun: + raise TypeError('Not prepared to commit') + self._checkTransaction(transaction) + self.transaction = None class SavePoint(object): diff -Nru transaction-2.4.0/transaction/tests/test__manager.py transaction-3.0.0/transaction/tests/test__manager.py --- transaction-2.4.0/transaction/tests/test__manager.py 2018-10-23 14:27:09.000000000 +0000 +++ transaction-3.0.0/transaction/tests/test__manager.py 2019-12-11 16:24:39.000000000 +0000 @@ -703,6 +703,19 @@ stopped.set() runner.join(1) + # The lack of the method below caused a test failure in one run + # -- caused indirectly by the failure of another test (this + # indicates that the tests in this suite are not fully isolated). + # However, defining the methods below reduced the "test coverage" + # once the initial test failure has been fixed. + # We therefore comment them out. + ## # the preceeding test (maybe others as well) registers `self` as + ## # synchronizer; satisfy the `ISynchronizer` requirements + ## def newTransaction(self, transaction): + ## pass + ## + ## beforeCompletion = afterCompletion = newTransaction + class TestThreadTransactionManager(unittest.TestCase): diff -Nru transaction-2.4.0/transaction/tests/test_register_compat.py transaction-3.0.0/transaction/tests/test_register_compat.py --- transaction-2.4.0/transaction/tests/test_register_compat.py 2018-10-23 14:27:09.000000000 +0000 +++ transaction-3.0.0/transaction/tests/test_register_compat.py 1970-01-01 00:00:00.000000000 +0000 @@ -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) diff -Nru transaction-2.4.0/transaction/tests/test__transaction.py transaction-3.0.0/transaction/tests/test__transaction.py --- transaction-2.4.0/transaction/tests/test__transaction.py 2018-10-23 14:27:09.000000000 +0000 +++ transaction-3.0.0/transaction/tests/test__transaction.py 2019-12-11 16:24:39.000000000 +0000 @@ -168,29 +168,6 @@ 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 @@ 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 @@ -615,7 +552,7 @@ 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 +572,7 @@ 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 @@ -683,7 +620,7 @@ from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey from transaction import _transaction - class _Synchrnonizers(object): + class _Synchronizers(object): def __init__(self, res): self._res = res def map(self, func): @@ -691,7 +628,7 @@ func(res) resources = [Resource('bbb', 'tpc_begin'), Resource('aaa', 'afterCompletion')] - sync = _Synchrnonizers(resources) + sync = _Synchronizers(resources) logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): txn = self._makeOne(sync) @@ -855,33 +792,65 @@ 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()), []) + self.assertIsNone(txn._manager) def test_abort_w_synchronizers(self): - from transaction.weakset import WeakSet from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey from transaction import _transaction + test = self class _Synch(object): - _before = _after = False + _before = _after = None def beforeCompletion(self, txn): self._before = txn + txn.set_data(self, 42) + test.assertIsNotNone(txn._manager) def afterCompletion(self, txn): self._after = txn - synchs = [_Synch(), _Synch(), _Synch()] - ws = WeakSet() - for synch in synchs: - ws.add(synch) + # data is accessible afterCompletion, + # but the transaction is not current anymore. + test.assertEqual(42, txn.data(self)) + test.assertIsNone(txn._manager) + class _BadSynch(_Synch): + def afterCompletion(self, txn): + _Synch.afterCompletion(self, txn) + raise SystemExit + + # Ensure iteration order + class Synchs(object): + synchs = [_Synch(), _Synch(), _Synch(), _BadSynch()] + def map(self, func): + for s in self.synchs: + func(s) + logger = DummyLogger() + class Manager(object): + txn = None + def free(self, txn): + test.assertIs(txn, self.txn) + self.txn = None + + manager = Manager() + synchs = Synchs() + with Monkey(_transaction, _LOGGER=logger): - txn = self._makeOne(synchronizers=ws) + txn = self._makeOne(synchronizers=synchs, manager=manager) + manager.txn = txn logger._clear() - txn.abort() - for synch in synchs: - self.assertTrue(synch._before is txn) - self.assertTrue(synch._after is txn) + with self.assertRaises(SystemExit): + txn.abort() + + for synch in synchs.synchs: + self.assertIs(synch._before, txn) + self.assertIs(synch._after, txn) + + # And everything was cleaned up despite raising the bad + # exception + self.assertIsNone(txn._manager) + self.assertIsNot(txn._synchronizers, synchs) + self.assertIsNone(manager.txn) def test_abort_w_afterCommitHooks(self): from transaction.tests.common import DummyLogger @@ -899,14 +868,14 @@ 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, []) + self.assertIsNone(txn._manager) - 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,13 +902,13 @@ 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) + self.assertIsNone(txn._manager) def test_abort_error_w_synchronizers(self): from transaction.weakset import WeakSet @@ -971,6 +940,46 @@ for synch in synchs: self.assertTrue(synch._before is t) self.assertTrue(synch._after is t) #called in _cleanup + self.assertIsNot(t._synchronizers, ws) + + def test_abort_synchronizer_error_w_resources(self): + from transaction.tests.common import DummyLogger + from transaction.tests.common import Monkey + from transaction import _transaction + class _Synch(object): + _before = _after = False + def beforeCompletion(self, txn): + self._before = txn + def afterCompletion(self, txn): + self._after = txn + + class _BadSynch(_Synch): + def beforeCompletion(self, txn): + _Synch.beforeCompletion(self, txn) + raise SystemExit + + # Ensure iteration order + class Synchs(object): + synchs = [_Synch(), _Synch(), _Synch(), _BadSynch()] + def map(self, func): + for s in self.synchs: + func(s) + + resource = Resource('a') + logger = DummyLogger() + synchs = Synchs() + with Monkey(_transaction, _LOGGER=logger): + t = self._makeOne(synchronizers=synchs) + logger._clear() + t._resources.append(resource) + with self.assertRaises(SystemExit): + t.abort() + + for synch in synchs.synchs: + self.assertTrue(synch._before is t) + self.assertTrue(synch._after is t) # called in _cleanup + self.assertIsNot(t._synchronizers, synchs) + self.assertTrue(resource._a) def test_abort_clears_resources(self): class DM(object): @@ -983,6 +992,178 @@ 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: @@ -1192,123 +1373,6 @@ 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): @@ -1322,132 +1386,6 @@ 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): @@ -1584,22 +1522,6 @@ 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. @@ -1627,14 +1549,14 @@ 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']) @@ -1643,7 +1565,7 @@ self.assertEqual(list(dm.keys()), []) @run_in_thread - def third(): + def _(): dm['a'] = 1 self.assertEqual(sync.log, ['1 new']) diff -Nru transaction-2.4.0/transaction/_transaction.py transaction-3.0.0/transaction/_transaction.py --- transaction-2.4.0/transaction/_transaction.py 2018-10-23 14:27:09.000000000 +0000 +++ transaction-3.0.0/transaction/_transaction.py 2019-12-11 16:24:39.000000000 +0000 @@ -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,12 +42,7 @@ 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: +class Status(object): # ACTIVE is the initial state. ACTIVE = "Active" @@ -63,11 +55,17 @@ # to commit or join this transaction will raise TransactionFailedError. COMMITFAILED = "Commit failed" -@implementer(interfaces.ITransaction, - interfaces.ITransactionDeprecated) -class Transaction(object): +class _NoSynchronizers(object): + @staticmethod + def map(_f): + "Does nothing" +@implementer(interfaces.ITransaction) +class Transaction(object): + """ + Default implementation of `~transaction.interfaces.ITransaction`. + """ # Assign an index to each savepoint so we can invalidate later savepoints # on rollback. The first index assigned is 1, and it goes up by 1 each @@ -79,7 +77,7 @@ _savepoint2index = None # Meta data. extended_info is also metadata, but is initialized to an - # emtpy dict in __init__. + # empty dict in __init__. _user = u"" _description = u"" @@ -120,6 +118,12 @@ # 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 @@ -150,12 +154,12 @@ self._description = text_or_warn(v) def isDoomed(self): - """ See ITransaction. + """ See `~transaction.interfaces.ITransaction`. """ return self.status is Status.DOOMED def doom(self): - """ See ITransaction. + """ See `~transaction.interfaces.ITransaction`. """ if self.status is not Status.DOOMED: if self.status is not Status.ACTIVE: @@ -174,7 +178,7 @@ self._failure_traceback.getvalue()) def join(self, resource): - """ See ITransaction. + """ See `~transaction.interfaces.ITransaction`. """ if self.status is Status.COMMITFAILED: self._prior_operation_failed() # doesn't return @@ -185,12 +189,6 @@ # 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: @@ -217,7 +215,7 @@ self._resources = [r for r in self._resources if r is not resource] def savepoint(self, optimistic=False): - """ See ITransaction. + """ See `~transaction.interfaces.ITransaction`. """ if self.status is Status.COMMITFAILED: self._prior_operation_failed() # doesn't return, it raises @@ -253,35 +251,8 @@ 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 persisent 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. + """ See `~transaction.interfaces.ITransaction`. """ if self.status is Status.DOOMED: raise interfaces.DoomedTransaction( @@ -312,9 +283,9 @@ 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): @@ -347,12 +318,12 @@ del t, v, tb def getBeforeCommitHooks(self): - """ See ITransaction. + """ See `~transaction.interfaces.ITransaction`. """ return iter(self._before_commit) def addBeforeCommitHook(self, hook, args=(), kws=None): - """ See ITransaction. + """ See `~transaction.interfaces.ITransaction`. """ if kws is None: kws = {} @@ -360,54 +331,101 @@ 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. + """ See `~transaction.interfaces.ITransaction`. """ return iter(self._after_commit) def addAfterCommitHook(self, hook, args=(), kws=None): - """ See ITransaction. + """ See `~transaction.interfaces.ITransaction`. """ if kws is None: kws = {} self._after_commit.append((hook, tuple(args), kws)) def _callAfterCommitHooks(self, status=True): - # Avoid to abort anything at the end if no hooks are registred. - if not self._after_commit: + 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 additional 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 registered. + 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 `~transaction.interfaces.ITransaction`. + """ + return iter(self._before_abort) + + def addBeforeAbortHook(self, hook, args=(), kws=None): + """ See `~transaction.interfaces.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 `~transaction.interfaces.ITransaction`. + """ + return iter(self._after_abort) + + def addAfterAbortHook(self, hook, args=(), kws=None): + """ See `~transaction.interfaces.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. @@ -463,20 +481,42 @@ except Exception: self.log.error("Error in tpc_abort() on manager %s", rm, exc_info=sys.exc_info()) + def _free_manager(self): + try: + if self._manager: + self._manager.free(self) + finally: + # If we try to abort a transaction and fail, the manager + # may have begun a new transaction, and will raise a + # ValueError from free(); we don't want that to happen + # again in _free(), which abort() always calls, so be sure + # to clear out the manager. + self._manager = None def _free(self): # Called when the transaction has been committed or aborted # 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 - if self._manager: - self._manager.free(self) + # All hooks and data are forgotten. + self._free_manager() if hasattr(self, '_data'): delattr(self, '_data') del self._resources[:] + del self._before_commit[:] + del self._after_commit[:] + del self._before_abort[:] + del self._after_abort[:] + + # self._synchronizers might be shared, we can't mutate it + self._synchronizers = _NoSynchronizers + self._adapters = None + self._voted = None + self.extension = None + def data(self, ob): try: data = self._data @@ -497,19 +537,24 @@ data[id(ob)] = ob_data def abort(self): - """ See ITransaction. + """ See `~transaction.interfaces.ITransaction`. """ - if self._savepoint2index: - self._invalidate_all_savepoints() - - self._synchronizers.map(lambda s: s.beforeCompletion(self)) - try: - t = None v = None tb = None + self._callBeforeAbortHooks() + if self._savepoint2index: + self._invalidate_all_savepoints() + + try: + self._synchronizers.map(lambda s: s.beforeCompletion(self)) + except: + t, v, tb = sys.exc_info() + self.log.error("Failed to call synchronizers", exc_info=sys.exc_info()) + + for rm in self._resources: try: rm.abort(self) @@ -519,7 +564,12 @@ self.log.error("Failed to abort resource manager: %s", rm, exc_info=sys.exc_info()) - self._free() + + self._callAfterAbortHooks() + # Unlike in commit(), we are no longer the current transaction + # when we call afterCompletion(). But we can't be completely _free(): + # the synchronizer might want to access some data it set before. + self._free_manager() self._synchronizers.map(lambda s: s.afterCompletion(self)) @@ -528,10 +578,11 @@ if tb is not None: reraise(t, v, tb) finally: + self._free() del t, v, tb def note(self, text): - """ See ITransaction. + """ See `~transaction.interfaces.ITransaction`. """ if text is not None: text = text_or_warn(text).strip() @@ -541,12 +592,12 @@ self.description = text def setUser(self, user_name, path=u"/"): - """ See ITransaction. + """ See `~transaction.interfaces.ITransaction`. """ self.user = u"%s %s" % (text_or_warn(path), text_or_warn(user_name)) def setExtendedInfo(self, name, value): - """ See ITransaction. + """ See `~transaction.interfaces.ITransaction`. """ self.extension[name] = value @@ -557,146 +608,15 @@ # 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: - """Transaction savepoint. +class Savepoint(object): + """Implementation of `~transaction.interfaces.ISavepoint`, a transaction savepoint. Transaction savepoints coordinate savepoints for data managers participating in a transaction. @@ -723,7 +643,7 @@ return self.transaction is not None def rollback(self): - """ See ISavepoint. + """ See `~transaction.interfaces.ISavepoint`. """ transaction = self.transaction if transaction is None: @@ -739,7 +659,7 @@ transaction._saveAndRaiseCommitishError() # reraises! -class AbortSavepoint: +class AbortSavepoint(object): def __init__(self, datamanager, transaction): self.datamanager = datamanager @@ -750,7 +670,7 @@ self.transaction._unjoin(self.datamanager) -class NoRollbackSavepoint: +class NoRollbackSavepoint(object): def __init__(self, datamanager): self.datamanager = datamanager diff -Nru transaction-2.4.0/transaction.egg-info/PKG-INFO transaction-3.0.0/transaction.egg-info/PKG-INFO --- transaction-2.4.0/transaction.egg-info/PKG-INFO 2018-10-23 14:27:09.000000000 +0000 +++ transaction-3.0.0/transaction.egg-info/PKG-INFO 2019-12-11 16:24:39.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: transaction -Version: 2.4.0 +Version: 3.0.0 Summary: Transaction management for Python Home-page: https://github.com/zopefoundation/transaction Author: Zope Corporation @@ -37,6 +37,42 @@ Changes ========= + 3.0.0 (2019-12-11) + ================== + + - Drop support for Python 3.4. + + - 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*) + (`#58 `_). + + It aborts the transaction now for all exceptions raised by *func* - even + 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 `_). + + - Make Transaction drop references to its hooks, manager, + synchronizers and data after a successful ``commit()`` and after + *any* ``abort()``. This helps avoid potential cyclic references. See + `issue 82 `_. + + - Allow synchronizers to access ``Transaction.data()`` when their + ``afterCompletion`` method is called while aborting a transaction. + + - Make it safe to call ``Transaction.abort()`` more than once. The + second and subsequent calls are no-ops. Previously a + ``ValueError(Foreign transaction)`` would be raised. + 2.4.0 (2018-10-23) ================== @@ -357,7 +393,7 @@ 1.0a1 (2007-12-18) ================== - = Initial release, branched from ZODB trunk on 2007-11-08 (aka + - Initial release, branched from ZODB trunk on 2007-11-08 (aka "3.9.0dev"). - Remove (deprecated) support for beforeCommitHook alias to @@ -380,13 +416,13 @@ Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Framework :: ZODB -Provides-Extra: testing -Provides-Extra: test Provides-Extra: docs +Provides-Extra: test +Provides-Extra: testing diff -Nru transaction-2.4.0/transaction.egg-info/SOURCES.txt transaction-3.0.0/transaction.egg-info/SOURCES.txt --- transaction-2.4.0/transaction.egg-info/SOURCES.txt 2018-10-23 14:27:09.000000000 +0000 +++ transaction-3.0.0/transaction.egg-info/SOURCES.txt 2019-12-11 16:24:39.000000000 +0000 @@ -13,6 +13,7 @@ tox.ini docs/Makefile docs/api.rst +docs/changes.rst docs/conf.py docs/convenience.rst docs/datamanager.rst @@ -22,7 +23,6 @@ docs/index.rst docs/integrations.rst docs/make.bat -docs/resourcemanager.rst docs/savepoint.rst docs/sqlalchemy.rst docs/_static/placeholder.txt @@ -46,6 +46,5 @@ transaction/tests/savepointsample.py transaction/tests/test__manager.py transaction/tests/test__transaction.py -transaction/tests/test_register_compat.py transaction/tests/test_savepoint.py transaction/tests/test_weakset.py \ No newline at end of file diff -Nru transaction-2.4.0/.travis.yml transaction-3.0.0/.travis.yml --- transaction-2.4.0/.travis.yml 2018-10-23 14:27:09.000000000 +0000 +++ transaction-3.0.0/.travis.yml 2019-12-11 16:24:39.000000000 +0000 @@ -1,25 +1,28 @@ language: python -sudo: false + +dist: xenial + python: - 2.7 - - 3.4 - 3.5 - 3.6 + - 3.7 + - 3.8 - pypy - pypy3 -matrix: - include: - - python: "3.7" - dist: xenial - sudo: true + install: - pip install zope.testrunner coverage coveralls - pip install -U -e .[test,docs] + script: - coverage run -m zope.testrunner --test-path=. -v - coverage run -a -m sphinx.cmd.build -b doctest -d docs/_build/doctrees docs docs/_build/doctest + notifications: email: false + after_success: - coveralls + cache: pip