././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1726644280.544209 transaction-5.0/0000755000076500000240000000000014672500071012567 5ustar00jensstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726644116.0 transaction-5.0/.pre-commit-config.yaml0000644000076500000240000000132714672477624017074 0ustar00jensstaff# Generated from: # https://github.com/zopefoundation/meta/tree/master/config/pure-python minimum_pre_commit_version: '3.6' repos: - repo: https://github.com/pycqa/isort rev: "5.13.2" hooks: - id: isort - repo: https://github.com/hhatto/autopep8 rev: "v2.3.1" hooks: - id: autopep8 args: [--in-place, --aggressive, --aggressive] - repo: https://github.com/asottile/pyupgrade rev: v3.17.0 hooks: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/isidentical/teyit rev: 0.4.3 hooks: - id: teyit - repo: https://github.com/PyCQA/flake8 rev: "7.1.1" hooks: - id: flake8 additional_dependencies: - flake8-debugger == 4.1.2 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726639933.0 transaction-5.0/.readthedocs.yaml0000644000076500000240000000123514672467475016043 0ustar00jensstaff# Generated from: # https://github.com/zopefoundation/meta/tree/master/config/pure-python # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-22.04 tools: python: "3.11" # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py # We recommend specifying your dependencies to enable reproducible builds: # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: install: - requirements: docs/requirements.txt - method: pip path: . ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726644142.0 transaction-5.0/CHANGES.rst0000644000076500000240000002716214672477656014427 0ustar00jensstaff========= Changes ========= 5.0 (2024-09-18) ================ - Add final support for Python 3.13. - Drop support for Python 3.7. 4.0 (2023-11-13) ================ - Drop support for Python 2.7, 3.5, 3.6. - Drop support for deprecated ``python setup.py test.``. - Add support for Python 3.12. - Add preliminary support for Python 3.13a2. 3.1.0 (2023-03-17) ================== - Add support for Python 3.9, 3.10, 3.11. 3.0.1 (2020-12-11) ================== - Exception raised by a before commit hook is no longer hidden. No further commit hooks are called and exception is propagated to the caller of ``commit()``. See `#95 `_. 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) ================== - Changed the implementation of ThreadTransactionManager to be a thread.local that wraps a TransactionManager rather than a thread.local that inherits from TransactionManager. It now exposes a manager attribute that allows access to the wrapped transaction manager to allow cross thread calls. See `issue 68 `_. 2.3.0 (2018-10-19) ================== - Add support for Python 3.7. - Reach 100% test coverage. - Fix ``transaction.manager.run`` formatting transaction notes when given a mix of byte and text strings, such as can happen in Python 2 with ``unicode_literals``. 2.2.1 (2018-03-27) ================== - Make documentation index more user friendly; move old docs to developer section. - Don't crash when printing tracebacks in IPython on Python 2. (This addresses https://github.com/zopefoundation/transaction/issues/5.) 2.2.0 (2018-02-27) ================== - Add support for Python 3.6. - Drop support for Python 3.3. - Add ``isRetryableError`` to the ``transaction.interfaces.ITransaction`` interface to allow external systems to query whether an exception is retryable (transient) by any of the attached data managers. Any ``transaction.interfaces.TransientError`` is considered retryable but a data manager may also consider other exceptions on a per-instance basis. See https://github.com/zopefoundation/transaction/pull/38 2.1.2 (2017-03-11) ================== - To avoid leaking memory, don't include unexpected value in warnings about non-text transaction meta data. 2.1.1 (2017-03-11) ================== - For backward compatibility, relax the requirements that transaction meta data (user or description) be text: - If None is assigned, the assignment is ignored. - If a non-text value is assigned, a warning is issued and the value is converted to text. If the value is a binary string, it will be decoded with the UTF-8 encoding the ``replace`` error policy. 2.1.0 (2017-02-08) ================== Added a transaction-manager explicit mode. Explicit mode makes some kinds of application bugs easier to detect and potentially allows data managers to manage resources more efficiently. (This addresses https://github.com/zopefoundation/transaction/issues/35.) 2.0.3 (2016-11-17) ================== - The user and description fields must now be set with text (unicode) data. Previously, if bytes were provided, they'd be decoded as ASCII. It was decided that this would lead to bugs that were hard to test for. Also, the transaction meta-data field, ``extended_info`` has been renamed to ``extension``. 2.0.2 (2016-11-13) ================== - Fixed: Some legacy applications expect the transaction _extension attribute to be mutable and it wasn't. 2.0.1 (2016-11-11) ================== - The transaction ``user`` and ``description`` attributes are now defined to be text (unicode) as opposed to Python the ``str`` type. - Added the ``extended_info`` transaction attribute which contains transaction meta data. (The ``_extension`` attribute is retained as an alias for backward compatibility.) The transaction interface, ``ITransaction``, now requires ``extended_info`` keys to be text (unicode) and values to be JSON-serializable. - Removed setUser from ITransaction. We'll keep the method indefinitely, but it's unseemly in ITransaction. :) The main purpose of these changes is to tighten up the text specification of user, description and extended_info keys, and to give us more flexibility in the future for serializing extended info. It's possible that these changes will be breaking, so we're also increasing the major version number. 1.7.0 (2016-11-08) ================== - Added a transaction-manager ``run`` method for running a function as a transaction, retrying as necessary on transient errors. - Fixed the transaction manager ``attempts`` method. It didn't stop repeating when there wasn't an error. - Corrected ITransaction by removing beforeCommitHook (which is no longer implemented) and removing 'self' from two methods. 1.6.1 (2016-06-10) ================== - Fixed: Synchonizers that registered with transaction managers when transactions were in progress didn't have their newTransaction methods called to let them know of the in-progress transactions. 1.6.0 (2016-05-21) ================== - New transaction API for storing data on behalf of objects, such as data managers. - Drop references to data managers joined to a transaction when it is committed or aborted. 1.5.0 (2016-05-05) ================== - Drop support for Python 2.6 and 3.2. - Add support for Python 3.5. - Added APIs for interogating and clearing internal state to support client tests. 1.4.4 (2015-05-19) ================== - Use the standard ``valuerefs()`` method rather than relying on implementation details of ``WeakValueDictionary`` in ``WeakSet``. - Add support for PyPy3. - Require 100% branch coverage (in addition to 100% statement coverage). 1.4.3 (2014-03-20) ================== - Add support for Python 3.4. 1.4.2 (skipped) =============== - Released in error as 1.4.3. 1.4.1 (2013-02-20) ================== - Document that values returned by ``sortKey`` must be strings, in order to guarantee total ordering. - Fix occasional RuntimeError: dictionary changed size during iteration errors in transaction.weakset on Python 3. 1.4.0 (2013-01-03) ================== - Updated Trove classifiers. 1.4.0b1 (2012-12-18) ==================== - Converted existing doctests into Sphinx documentation (snippets are exercised via 'tox'). - 100% unit test coverage. - Backward incompatibility: raise ValueError rather than AssertionError for runtime errors: - In ``Transaction.doom`` if the transaction is in a non-doomable state. - In ``TransactionManager.attempts`` if passed a non-positive value. - In ``TransactionManager.free`` if passed a foreign transaction. - Declared support for Python 3.3 in ``setup.py``, and added ``tox`` testing. - When a non-retryable exception was raised as the result of a call to ``transaction.manager.commit`` within the "attempts" machinery, the exception was not reraised properly. Symptom: an unrecoverable exception such as ``Unsupported: Storing blobs in is not supported.`` would be swallowed inappropriately. 1.3.0 (2012-05-16) ================== - Added Sphinx API docuementation. - Added explicit support for PyPy. - Dropped use of Python3-impatible ``zope.interface.implements`` class advisor in favor of ``zope.interface.implementer`` class decorator. - Added support for continuous integration using ``tox`` and ``jenkins``. - Added ``setup.py docs`` alias (installs ``Sphinx`` and dependencies). - Added ``setup.py dev`` alias (runs ``setup.py develop`` plus installs ``nose`` and ``coverage``). - Python 3.3 compatibility. - Fix "for attempt in transaction.attempts(x)" machinery, which would not retry a transaction if its implicit call to ``.commit()`` itself raised a transient error. Symptom: seeing conflict errors even though you thought you were retrying some number of times via the "attempts" machinery (the first attempt to generate an exception during commit would cause that exception to be raised). 1.2.0 (2011-12-05) ================== New Features: - Python 3.2 compatibility. - Dropped Python 2.4 and 2.5 compatibility (use 1.1.1 if you need to use "transaction" under these Python versions). 1.1.1 (2010-09-16) ================== Bug Fixes: - Code in ``_transaction.py`` held on to local references to traceback objects after calling ``sys.exc_info()`` to get one, causing potential reference leakages. - Fixed ``hexlify`` NameError in ``transaction._transaction.oid_repr`` and add test. 1.1.0 (1010-05-12) ================== New Features: - Transaction managers and the transaction module can be used with the with statement to define transaction boundaries, as in:: with transaction: ... do some things ... See transaction/tests/convenience.txt for more details. - There is a new iterator function that automates dealing with transient errors (such as ZODB confict errors). For example, in:: for attempt in transaction.attempts(5): with attempt: ... do some things .. If the work being done raises transient errors, the transaction will be retried up to 5 times. See transaction/tests/convenience.txt for more details. Bugs fixed: - Fixed a bug that caused extra commit calls to be made on data managers under certain special circumstances. https://mail.zope.org/pipermail/zodb-dev/2010-May/013329.html - When threads were reused, transaction data could leak accross them, causing subtle application bugs. https://bugs.launchpad.net/zodb/+bug/239086 1.0.1 (2010-05-07) ================== - LP #142464: remove double newline between log entries: it makes doing smarter formatting harder. - Updated tests to remove use of deprecated ``zope.testing.doctest``. 1.0.0 (2009-07-24) ================== - Fix test that incorrectly relied on the order of a list that was generated from a dict. - Remove crufty DEPENDENCIES.cfg left over from zpkg. 1.0a1 (2007-12-18) ================== - Initial release, branched from ZODB trunk on 2007-11-08 (aka "3.9.0dev"). - Remove (deprecated) support for beforeCommitHook alias to addBeforeCommitHook. - Add weakset tests. - Remove unit tests that depend on ZODB.tests.utils from test_transaction (these are actually integration tests). ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726639933.0 transaction-5.0/CONTRIBUTING.md0000644000076500000240000000144414672467475015047 0ustar00jensstaff # Contributing to zopefoundation projects The projects under the zopefoundation GitHub organization are open source and welcome contributions in different forms: * bug reports * code improvements and bug fixes * documentation improvements * pull request reviews For any changes in the repository besides trivial typo fixes you are required to sign the contributor agreement. See https://www.zope.dev/developer/becoming-a-committer.html for details. Please visit our [Developer Guidelines](https://www.zope.dev/developer/guidelines.html) if you'd like to contribute code changes and our [guidelines for reporting bugs](https://www.zope.dev/developer/reporting-bugs.html) if you want to file a bug report. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/COPYRIGHT.txt0000644000076500000240000000004014632330173014672 0ustar00jensstaffZope Foundation and Contributors././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/LICENSE.txt0000644000076500000240000000402614632330173014414 0ustar00jensstaffZope Public License (ZPL) Version 2.1 A copyright notice accompanies this license document that identifies the copyright holders. This license has been certified as open source. It has also been designated as GPL compatible by the Free Software Foundation (FSF). Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions in source code must retain the accompanying copyright notice, this list of conditions, and the following disclaimer. 2. Redistributions in binary form must reproduce the accompanying copyright notice, this list of conditions, and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Names of the copyright holders must not be used to endorse or promote products derived from this software without prior written permission from the copyright holders. 4. The right to distribute this software or to use it for any purpose does not give you the right to use Servicemarks (sm) or Trademarks (tm) of the copyright holders. Use of them is covered by separate agreement with the copyright holders. 5. If any files are modified, you must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. Disclaimer THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726644116.0 transaction-5.0/MANIFEST.in0000644000076500000240000000060714672477624014351 0ustar00jensstaff# Generated from: # https://github.com/zopefoundation/meta/tree/master/config/pure-python include *.md include *.rst include *.txt include buildout.cfg include tox.ini include .pre-commit-config.yaml recursive-include docs *.py recursive-include docs *.rst recursive-include docs *.txt recursive-include docs Makefile recursive-include src *.py include *.yaml recursive-include docs *.bat ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1726644280.5441134 transaction-5.0/PKG-INFO0000644000076500000240000003407614672500071013676 0ustar00jensstaffMetadata-Version: 2.1 Name: transaction Version: 5.0 Summary: Transaction management for Python Home-page: https://github.com/zopefoundation/transaction Author: Zope Foundation and Contributors Author-email: zodb-dev@zope.dev License: ZPL 2.1 Project-URL: Issue Tracker, https://github.com/zopefoundation/transaction/issues Project-URL: Sources, https://github.com/zopefoundation/transaction Platform: any Classifier: Development Status :: 6 - Mature Classifier: License :: OSI Approved :: Zope Public License Classifier: Programming Language :: Python Classifier: Topic :: Database Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: Unix Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Framework :: ZODB Requires-Python: >=3.8 Description-Content-Type: text/x-rst License-File: LICENSE.txt Requires-Dist: zope.interface Provides-Extra: docs Requires-Dist: Sphinx; extra == "docs" Requires-Dist: repoze.sphinx.autointerface; extra == "docs" Provides-Extra: testing Requires-Dist: coverage; extra == "testing" ============ Transactions ============ .. image:: https://github.com/zopefoundation/transaction/actions/workflows/tests.yml/badge.svg :target: https://github.com/zopefoundation/transaction/actions/workflows/tests.yml .. image:: https://readthedocs.org/projects/transaction/badge/?version=latest :target: http://transaction.readthedocs.org/en/latest/ :alt: Documentation Status .. image:: https://img.shields.io/pypi/v/transaction.svg :target: https://pypi.python.org/pypi/transaction :alt: PyPI .. image:: https://img.shields.io/pypi/pyversions/transaction.svg :target: https://pypi.python.org/pypi/transaction :alt: Python versions This package contains a generic transaction implementation for Python. It is mainly used by the ZODB. See http://transaction.readthedocs.org/en/latest for narrative documentation on its usage. ========= Changes ========= 5.0 (2024-09-18) ================ - Add final support for Python 3.13. - Drop support for Python 3.7. 4.0 (2023-11-13) ================ - Drop support for Python 2.7, 3.5, 3.6. - Drop support for deprecated ``python setup.py test.``. - Add support for Python 3.12. - Add preliminary support for Python 3.13a2. 3.1.0 (2023-03-17) ================== - Add support for Python 3.9, 3.10, 3.11. 3.0.1 (2020-12-11) ================== - Exception raised by a before commit hook is no longer hidden. No further commit hooks are called and exception is propagated to the caller of ``commit()``. See `#95 `_. 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) ================== - Changed the implementation of ThreadTransactionManager to be a thread.local that wraps a TransactionManager rather than a thread.local that inherits from TransactionManager. It now exposes a manager attribute that allows access to the wrapped transaction manager to allow cross thread calls. See `issue 68 `_. 2.3.0 (2018-10-19) ================== - Add support for Python 3.7. - Reach 100% test coverage. - Fix ``transaction.manager.run`` formatting transaction notes when given a mix of byte and text strings, such as can happen in Python 2 with ``unicode_literals``. 2.2.1 (2018-03-27) ================== - Make documentation index more user friendly; move old docs to developer section. - Don't crash when printing tracebacks in IPython on Python 2. (This addresses https://github.com/zopefoundation/transaction/issues/5.) 2.2.0 (2018-02-27) ================== - Add support for Python 3.6. - Drop support for Python 3.3. - Add ``isRetryableError`` to the ``transaction.interfaces.ITransaction`` interface to allow external systems to query whether an exception is retryable (transient) by any of the attached data managers. Any ``transaction.interfaces.TransientError`` is considered retryable but a data manager may also consider other exceptions on a per-instance basis. See https://github.com/zopefoundation/transaction/pull/38 2.1.2 (2017-03-11) ================== - To avoid leaking memory, don't include unexpected value in warnings about non-text transaction meta data. 2.1.1 (2017-03-11) ================== - For backward compatibility, relax the requirements that transaction meta data (user or description) be text: - If None is assigned, the assignment is ignored. - If a non-text value is assigned, a warning is issued and the value is converted to text. If the value is a binary string, it will be decoded with the UTF-8 encoding the ``replace`` error policy. 2.1.0 (2017-02-08) ================== Added a transaction-manager explicit mode. Explicit mode makes some kinds of application bugs easier to detect and potentially allows data managers to manage resources more efficiently. (This addresses https://github.com/zopefoundation/transaction/issues/35.) 2.0.3 (2016-11-17) ================== - The user and description fields must now be set with text (unicode) data. Previously, if bytes were provided, they'd be decoded as ASCII. It was decided that this would lead to bugs that were hard to test for. Also, the transaction meta-data field, ``extended_info`` has been renamed to ``extension``. 2.0.2 (2016-11-13) ================== - Fixed: Some legacy applications expect the transaction _extension attribute to be mutable and it wasn't. 2.0.1 (2016-11-11) ================== - The transaction ``user`` and ``description`` attributes are now defined to be text (unicode) as opposed to Python the ``str`` type. - Added the ``extended_info`` transaction attribute which contains transaction meta data. (The ``_extension`` attribute is retained as an alias for backward compatibility.) The transaction interface, ``ITransaction``, now requires ``extended_info`` keys to be text (unicode) and values to be JSON-serializable. - Removed setUser from ITransaction. We'll keep the method indefinitely, but it's unseemly in ITransaction. :) The main purpose of these changes is to tighten up the text specification of user, description and extended_info keys, and to give us more flexibility in the future for serializing extended info. It's possible that these changes will be breaking, so we're also increasing the major version number. 1.7.0 (2016-11-08) ================== - Added a transaction-manager ``run`` method for running a function as a transaction, retrying as necessary on transient errors. - Fixed the transaction manager ``attempts`` method. It didn't stop repeating when there wasn't an error. - Corrected ITransaction by removing beforeCommitHook (which is no longer implemented) and removing 'self' from two methods. 1.6.1 (2016-06-10) ================== - Fixed: Synchonizers that registered with transaction managers when transactions were in progress didn't have their newTransaction methods called to let them know of the in-progress transactions. 1.6.0 (2016-05-21) ================== - New transaction API for storing data on behalf of objects, such as data managers. - Drop references to data managers joined to a transaction when it is committed or aborted. 1.5.0 (2016-05-05) ================== - Drop support for Python 2.6 and 3.2. - Add support for Python 3.5. - Added APIs for interogating and clearing internal state to support client tests. 1.4.4 (2015-05-19) ================== - Use the standard ``valuerefs()`` method rather than relying on implementation details of ``WeakValueDictionary`` in ``WeakSet``. - Add support for PyPy3. - Require 100% branch coverage (in addition to 100% statement coverage). 1.4.3 (2014-03-20) ================== - Add support for Python 3.4. 1.4.2 (skipped) =============== - Released in error as 1.4.3. 1.4.1 (2013-02-20) ================== - Document that values returned by ``sortKey`` must be strings, in order to guarantee total ordering. - Fix occasional RuntimeError: dictionary changed size during iteration errors in transaction.weakset on Python 3. 1.4.0 (2013-01-03) ================== - Updated Trove classifiers. 1.4.0b1 (2012-12-18) ==================== - Converted existing doctests into Sphinx documentation (snippets are exercised via 'tox'). - 100% unit test coverage. - Backward incompatibility: raise ValueError rather than AssertionError for runtime errors: - In ``Transaction.doom`` if the transaction is in a non-doomable state. - In ``TransactionManager.attempts`` if passed a non-positive value. - In ``TransactionManager.free`` if passed a foreign transaction. - Declared support for Python 3.3 in ``setup.py``, and added ``tox`` testing. - When a non-retryable exception was raised as the result of a call to ``transaction.manager.commit`` within the "attempts" machinery, the exception was not reraised properly. Symptom: an unrecoverable exception such as ``Unsupported: Storing blobs in is not supported.`` would be swallowed inappropriately. 1.3.0 (2012-05-16) ================== - Added Sphinx API docuementation. - Added explicit support for PyPy. - Dropped use of Python3-impatible ``zope.interface.implements`` class advisor in favor of ``zope.interface.implementer`` class decorator. - Added support for continuous integration using ``tox`` and ``jenkins``. - Added ``setup.py docs`` alias (installs ``Sphinx`` and dependencies). - Added ``setup.py dev`` alias (runs ``setup.py develop`` plus installs ``nose`` and ``coverage``). - Python 3.3 compatibility. - Fix "for attempt in transaction.attempts(x)" machinery, which would not retry a transaction if its implicit call to ``.commit()`` itself raised a transient error. Symptom: seeing conflict errors even though you thought you were retrying some number of times via the "attempts" machinery (the first attempt to generate an exception during commit would cause that exception to be raised). 1.2.0 (2011-12-05) ================== New Features: - Python 3.2 compatibility. - Dropped Python 2.4 and 2.5 compatibility (use 1.1.1 if you need to use "transaction" under these Python versions). 1.1.1 (2010-09-16) ================== Bug Fixes: - Code in ``_transaction.py`` held on to local references to traceback objects after calling ``sys.exc_info()`` to get one, causing potential reference leakages. - Fixed ``hexlify`` NameError in ``transaction._transaction.oid_repr`` and add test. 1.1.0 (1010-05-12) ================== New Features: - Transaction managers and the transaction module can be used with the with statement to define transaction boundaries, as in:: with transaction: ... do some things ... See transaction/tests/convenience.txt for more details. - There is a new iterator function that automates dealing with transient errors (such as ZODB confict errors). For example, in:: for attempt in transaction.attempts(5): with attempt: ... do some things .. If the work being done raises transient errors, the transaction will be retried up to 5 times. See transaction/tests/convenience.txt for more details. Bugs fixed: - Fixed a bug that caused extra commit calls to be made on data managers under certain special circumstances. https://mail.zope.org/pipermail/zodb-dev/2010-May/013329.html - When threads were reused, transaction data could leak accross them, causing subtle application bugs. https://bugs.launchpad.net/zodb/+bug/239086 1.0.1 (2010-05-07) ================== - LP #142464: remove double newline between log entries: it makes doing smarter formatting harder. - Updated tests to remove use of deprecated ``zope.testing.doctest``. 1.0.0 (2009-07-24) ================== - Fix test that incorrectly relied on the order of a list that was generated from a dict. - Remove crufty DEPENDENCIES.cfg left over from zpkg. 1.0a1 (2007-12-18) ================== - Initial release, branched from ZODB trunk on 2007-11-08 (aka "3.9.0dev"). - Remove (deprecated) support for beforeCommitHook alias to addBeforeCommitHook. - Add weakset tests. - Remove unit tests that depend on ZODB.tests.utils from test_transaction (these are actually integration tests). ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/README.rst0000644000076500000240000000157114632330173014262 0ustar00jensstaff============ Transactions ============ .. image:: https://github.com/zopefoundation/transaction/actions/workflows/tests.yml/badge.svg :target: https://github.com/zopefoundation/transaction/actions/workflows/tests.yml .. image:: https://readthedocs.org/projects/transaction/badge/?version=latest :target: http://transaction.readthedocs.org/en/latest/ :alt: Documentation Status .. image:: https://img.shields.io/pypi/v/transaction.svg :target: https://pypi.python.org/pypi/transaction :alt: PyPI .. image:: https://img.shields.io/pypi/pyversions/transaction.svg :target: https://pypi.python.org/pypi/transaction :alt: Python versions This package contains a generic transaction implementation for Python. It is mainly used by the ZODB. See http://transaction.readthedocs.org/en/latest for narrative documentation on its usage. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1726644280.5359924 transaction-5.0/docs/0000755000076500000240000000000014672500071013517 5ustar00jensstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/Makefile0000644000076500000240000001272014632330173015161 0ustar00jensstaff# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/transaction.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/transaction.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/transaction" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/transaction" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1726644280.5308836 transaction-5.0/docs/_build/0000755000076500000240000000000014672500071014755 5ustar00jensstaff././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1726644280.5362635 transaction-5.0/docs/_build/doctest/0000755000076500000240000000000014672500071016422 5ustar00jensstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726639936.0 transaction-5.0/docs/_build/doctest/output.txt0000644000076500000240000000170414672467500020536 0ustar00jensstaffResults of doctest builder run on 2024-09-18 08:12:16 ===================================================== Document: convenience --------------------- 1 items passed all tests: 8 tests in default 8 tests in 1 items. 8 passed and 0 failed. Test passed. Document: hooks --------------- 1 items passed all tests: 134 tests in default 134 tests in 1 items. 134 passed and 0 failed. Test passed. Document: doom -------------- 1 items passed all tests: 32 tests in default 32 tests in 1 items. 32 passed and 0 failed. Test passed. Document: datamanager --------------------- 1 items passed all tests: 161 tests in default 161 tests in 1 items. 161 passed and 0 failed. Test passed. Document: savepoint ------------------- 1 items passed all tests: 76 tests in default 76 tests in 1 items. 76 passed and 0 failed. Test passed. Doctest summary =============== 411 tests 0 failures in tests 0 failures in setup code 0 failures in cleanup code ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1726644280.531008 transaction-5.0/docs/_build/html/0000755000076500000240000000000014672500071015721 5ustar00jensstaff././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1726644280.5393693 transaction-5.0/docs/_build/html/_sources/0000755000076500000240000000000014672500071017543 5ustar00jensstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/_build/html/_sources/api.rst.txt0000644000076500000240000000204714632330173021667 0ustar00jensstaff=============================== ``transaction`` API Reference =============================== Interfaces ========== .. module:: transaction.interfaces .. autointerface:: ITransactionManager .. autointerface:: ITransaction .. autointerface:: IDataManager .. autointerface:: ISavepointDataManager .. autointerface:: IRetryDataManager .. autointerface:: IDataManagerSavepoint .. autointerface:: ISavepoint .. autointerface:: ISynchronizer Exceptions ---------- .. autoclass:: TransactionError .. autoclass:: TransactionFailedError .. autoclass:: DoomedTransaction .. autoclass:: TransientError .. autoclass:: InvalidSavepointRollbackError .. autoclass:: NoTransaction .. autoclass:: AlreadyInTransaction API Objects =========== .. automodule:: transaction .. autoclass:: Transaction .. autoclass:: TransactionManager .. automethod:: __enter__ Alias for :meth:`get` .. automethod:: __exit__ On error, aborts the current transaction. Otherwise, commits. .. autoclass:: ThreadTransactionManager .. autoclass:: Savepoint ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/_build/html/_sources/changes.rst.txt0000644000076500000240000000003414632330173022520 0ustar00jensstaff.. include:: ../CHANGES.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/_build/html/_sources/convenience.rst.txt0000644000076500000240000000653514632330173023420 0ustar00jensstaff================================= Transaction convenience support ================================= with support ============ We can now use the with statement to define transaction boundaries. .. doctest:: >>> import transaction.tests.savepointsample >>> dm = transaction.tests.savepointsample.SampleSavepointDataManager() >>> list(dm.keys()) [] We can use it with a manager: .. doctest:: >>> with transaction.manager as t: ... dm['z'] = 3 ... t.note(u'test 3') >>> dm['z'] 3 >>> dm.last_note == 'test 3' True >>> with transaction.manager: #doctest ELLIPSIS ... dm['z'] = 4 ... xxx Traceback (most recent call last): ... NameError: ... name 'xxx' is not defined >>> dm['z'] 3 On Python 2, you can also abbreviate ``with transaction.manager:`` as ``with transaction:``. This does not work on Python 3 (see http://bugs.python.org/issue12022). Retries ======= Commits can fail for transient reasons, especially conflicts. Applications will often retry transactions some number of times to overcome transient failures. This typically looks something like:: for i in range(3): try: with transaction.manager: ... some something ... except SomeTransientException: continue else: break This is rather ugly and easy to get wrong. Transaction managers provide two helpers for this case. Running and retrying functions as transactions ---------------------------------------------- The first helper runs a function as a transaction:: def do_somthing(): "Do something" ... some something ... transaction.manager.run(do_somthing) You can also use this as a decorator, which executes the decorated function immediately [#decorator-executes]_:: @transaction.manager.run def _(): "Do something" ... some something ... The transaction manager ``run`` method will run the function and return the results. If the function raises a ``TransientError``, the function will be retried a configurable number of times, 3 by default. Any other exceptions will be raised. The function name (if it isn't ``'_'``) and docstring, if any, are added to the transaction description. You can pass an integer number of times to try to the ``run`` method:: transaction.manager.run(do_somthing, 9) @transaction.manager.run(9) def _(): "Do something" ... some something ... 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:: for attempt in transaction.manager.attempts(): with attempt as t: ... some something ... This runs the code block until it runs without a transient error or until the number of attempts is exceeded. By default, it tries 3 times, but you can pass a number of attempts:: for attempt in transaction.manager.attempts(9): with attempt as t: ... some something ... .. [#decorator-executes] Some people find this easier to read, even though the result isn't a decorated function, but rather the result of calling it in a transaction. The function name ``_`` is used here to emphasize that the function is essentially being used as an anonymous function. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/_build/html/_sources/datamanager.rst.txt0000644000076500000240000002413114632330173023360 0ustar00jensstaff======================== Writing a Data Manager ======================== .. currentmodule:: transaction.interfaces Simple Data Manager =================== .. doctest:: >>> from transaction.tests.examples import DataManager 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:: >>> rm = DataManager() It has two pieces, 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 ``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. The sample data manager updates the state when we call ``tpc_vote``, after calling ``commit``: .. doctest:: >>> t1 = '1' >>> rm.tpc_begin(t1) >>> rm.state, rm.delta (0, 1) >>> rm.commit(t1) >>> rm.tpc_vote(t1) >>> rm.state, rm.delta (1, 1) Now if we call tpc_finish: .. doctest:: >>> 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 data manager 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 >>> 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 ``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 ``tpc_finish`` Method ========================= Complete two-phase commit .. doctest:: >>> rm = DataManager() >>> rm.state 0 >>> rm.inc() We start two-phase commit by calling ``tpc_begin``, ``commit``, and ``tpc_vote``: >>> t1 = '1' >>> 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 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 ``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: ('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 ``abort`` method ==================== The abort method can be called before two-phase commit to throw away work done in the transaction: .. doctest:: >>> dm = DataManager() >>> dm.inc() >>> dm.state, dm.delta (0, 1) >>> t1 = '1' >>> dm.abort(t1) >>> dm.state, dm.delta (0, 0) The abort method also throws away work done in savepoints: .. doctest:: >>> dm.inc() >>> r = dm.savepoint(t1) >>> dm.inc() >>> r = dm.savepoint(t1) >>> dm.state, dm.delta (0, 2) >>> dm.abort(t1) >>> dm.state, dm.delta (0, 0) If savepoints are used, abort must be passed the same transaction: .. doctest:: >>> dm.inc() >>> r = dm.savepoint(t1) >>> t2 = '2' >>> dm.abort(t2) Traceback (most recent call last): ... TypeError: ('Transaction missmatch', '2', '1') >>> dm.abort(t1) 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.tpc_begin(t1) >>> dm.abort(t2) Traceback (most recent call last): ... TypeError: ('Transaction missmatch', '2', '1') >>> dm.abort(t1) The ``commit`` method ===================== Called after ``tpc_begin`` to make changes persistent and prepare for voting. .. doctest:: >>> dm = DataManager() >>> dm.state 0 >>> dm.inc() We start two-phase commit by calling ``tpc_begin`` .. doctest:: >>> t1 = '1' >>> dm.tpc_begin(t1) 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 to call commit without calling ``tpc_begin`` first: .. doctest:: >>> dm = DataManager() >>> t2 = '2' >>> dm.commit(t2) Traceback (most recent call last): ... TypeError: Not prepared to commit If course, the transactions given to ``tpc_begin`` and commit must be the same: .. doctest:: >>> dm = DataManager() >>> t3 = '3' >>> dm.tpc_begin(t3) >>> dm.commit(t2) Traceback (most recent call last): ... TypeError: ('Transaction missmatch', '2', '3') The ``savepoint`` Method ======================== Provide the ability to rollback transaction state Savepoints provide a way to: - Save partial transaction work. For some data managers, this could allow resources to be used more efficiently. - Provide the ability to revert state to a point in a transaction without aborting the entire transaction. In other words, savepoints support partial aborts. Savepoints don't use two-phase commit. If there are errors in setting or rolling back to savepoints, the application should abort the containing transaction. This is *not* the responsibility of the data manager. Savepoints are always associated with a transaction. Any work done in a savepoint's transaction is tentative until the transaction is committed using two-phase commit. .. doctest:: >>> rm = DataManager() >>> 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/_build/html/_sources/developer.rst.txt0000644000076500000240000001204314632330173023100 0ustar00jensstaff================================= ``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 calls the following four methods on each resource manager; it calls `IDataManager.tpc_begin` on each resource manager before calling `IDataManager.commit` on any of them. 1. tpc_begin(txn) 2. commit(txn) 3. tpc_vote(txn) 4. tpc_finish(txn) Before-commit hook ================== Sometimes, applications want to execute some code when a transaction is committed. For example, one might want to delay object indexing until a transaction commits, rather than indexing every time an object is changed. Or someone might want to check invariants only after a set of operations. A pre-commit hook is available for such use cases: use `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 =========== 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 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 and calling ``commit()`` or ``abort()`` implicitly begins a new one. This behavior can be convenient for interactive use, but invites subtle bugs: - Calling begin() without realizing that there are outstanding changes that will be aborted. - Interacting with a database without controlling transactions, in which case changes may be unexpectedly discarded. For applications, including frameworks that control transactions, transaction managers provide an optional explicit mode. Transaction managers have an ``explicit`` constructor keyword argument that, if True puts the transaction manager in explicit mode. In explicit mode: - It is an error to call ``get()``, ``commit()``, ``abort()``, ``doom()``, ``isDoomed``, or ``savepoint()`` without a preceding ``begin()`` call. Doing so will raise a ``NoTransaction`` exception. - It is an error to call ``begin()`` after a previous ``begin()`` without an intervening ``commit()`` or ``abort()`` call. Doing so will raise an ``AlreadyInTransaction`` exception. In explicit mode, bugs like those mentioned above are much easier to avoid because they cause explicit exceptions that can typically be caught in development. An additional benefit of explicit mode is that it can allow data managers to manage resources more efficiently. Transaction managers have an explicit attribute that can be queried to determine if explicit mode is enabled. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/_build/html/_sources/doom.rst.txt0000644000076500000240000001050214632330173022047 0ustar00jensstaffDooming Transactions ==================== A doomed transaction behaves exactly the same way as an active transaction but raises an error on any attempt to commit it, thus forcing an abort. Doom is useful in places where abort is unsafe and an exception cannot be raised. This occurs when the programmer wants the code following the doom to run but not commit. It is unsafe to abort in these circumstances as a following get() may implicitly open a new transaction. Any attempt to commit a doomed transaction will raise a DoomedTransaction exception. An example of such a use case can be found in zope/app/form/browser/editview.py. Here a form validation failure must doom the transaction as committing the transaction may have side-effects. However, the form code must continue to calculate a form containing the error messages to return. For Zope in general, code running within a request should always doom transactions rather than aborting them. It is the responsibilty of the publication to either abort() or commit() the transaction. Application code can use savepoints and doom() safely. To see how it works we first need to create a stub data manager: .. doctest:: >>> from transaction.interfaces import IDataManager >>> from zope.interface import implementer >>> @implementer(IDataManager) ... class DataManager: ... def __init__(self): ... self.attr_counter = {} ... def __getattr__(self, name): ... def f(transaction): ... self.attr_counter[name] = self.attr_counter.get(name, 0) + 1 ... return f ... def total(self): ... count = 0 ... for access_count in self.attr_counter.values(): ... count += access_count ... return count ... def sortKey(self): ... return '1' Start a new transaction: .. doctest:: >>> import transaction >>> txn = transaction.begin() >>> dm = DataManager() >>> txn.join(dm) We can ask a transaction if it is doomed to avoid expensive operations. An example of a use case is an object-relational mapper where a pre-commit hook sends all outstanding SQL to a relational database for objects changed during the transaction. This expensive operation is not necessary if the transaction has been doomed. A non-doomed transaction should return False: .. doctest:: >>> txn.isDoomed() False We can doom a transaction by calling .doom() on it: .. doctest:: >>> txn.doom() >>> txn.isDoomed() True We can doom it again if we like: .. doctest:: >>> txn.doom() The data manager is unchanged at this point: .. doctest:: >>> dm.total() 0 Attempting to commit a doomed transaction any number of times raises a DoomedTransaction: .. doctest:: >>> txn.commit() # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): DoomedTransaction: transaction doomed, cannot commit >>> txn.commit() # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): DoomedTransaction: transaction doomed, cannot commit But still leaves the data manager unchanged: .. doctest:: >>> dm.total() 0 But the doomed transaction can be aborted: .. doctest:: >>> txn.abort() Which aborts the data manager: .. doctest:: >>> dm.total() 1 >>> dm.attr_counter['abort'] 1 Dooming the current transaction can also be done directly from the transaction module. We can also begin a new transaction directly after dooming the old one: .. doctest:: >>> txn = transaction.begin() >>> transaction.isDoomed() False >>> transaction.doom() >>> transaction.isDoomed() True >>> txn = transaction.begin() After committing a transaction we get an assertion error if we try to doom the transaction. This could be made more specific, but trying to doom a transaction after it's been committed is probably a programming error: .. doctest:: >>> txn = transaction.begin() >>> txn.commit() >>> txn.doom() Traceback (most recent call last): ... ValueError: non-doomable A doomed transaction should act the same as an active transaction, so we should be able to join it: .. doctest:: >>> txn = transaction.begin() >>> txn.doom() >>> dm2 = DataManager() >>> txn.join(dm2) Clean up: .. doctest:: >>> txn = transaction.begin() >>> txn.abort() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/_build/html/_sources/hooks.rst.txt0000644000076500000240000002352114632330173022241 0ustar00jensstaffHooking the Transaction Machinery ================================= The :mod:`transaction` machinery allows application developers to register two different groups of callbacks to be called, one group before committing the transaction and one group after. These hooks are **not** designed to be used as replacements for the two-phase commit machinery defined by a resource manager (see :doc:`resourcemanager`). In particular, hook functions **must not** raise or propagate exceptions. .. warning:: Hook functions which *do* raise or propagate exceptions will leave the application in an undefined state. The :meth:`addBeforeCommitHook` Method -------------------------------------- Let's define a hook to call, and a way to see that it was called. .. doctest:: >>> log = [] >>> def reset_log(): ... del log[:] >>> def hook(arg='no_arg', kw1='no_kw1', kw2='no_kw2'): ... log.append("arg %r kw1 %r kw2 %r" % (arg, kw1, kw2)) Now register the hook with a transaction. .. doctest:: >>> from transaction import begin >>> import transaction >>> t = begin() >>> t.addBeforeCommitHook(hook, ('1',)) We can see that the hook is indeed registered. .. doctest:: >>> [(hook.__name__, args, kws) ... for hook, args, kws in t.getBeforeCommitHooks()] [('hook', ('1',), {})] When transaction commit starts, the hook is called, with its arguments. .. doctest:: >>> log [] >>> t.commit() >>> log ["arg '1' kw1 'no_kw1' kw2 'no_kw2'"] >>> reset_log() A hook's registration is consumed whenever the hook is called. Since the hook above was called, it's no longer registered: .. doctest:: >>> from transaction import commit >>> len(list(t.getBeforeCommitHooks())) 0 >>> commit() >>> log [] The hook is only called for a full commit, not for a savepoint. .. doctest:: >>> t = begin() >>> t.addBeforeCommitHook(hook, ('A',), dict(kw1='B')) >>> dummy = t.savepoint() >>> log [] >>> t.commit() >>> log ["arg 'A' kw1 'B' kw2 'no_kw2'"] >>> reset_log() If a transaction is aborted, no hook is called. .. doctest:: >>> from transaction import abort >>> t = begin() >>> t.addBeforeCommitHook(hook, ["OOPS!"]) >>> abort() >>> log [] >>> commit() >>> log [] The hook is called before the commit does anything, so even if the commit fails the hook will have been called. To provoke failures in commit, we'll add failing resource manager to the transaction. .. doctest:: >>> class CommitFailure(Exception): ... pass >>> class FailingDataManager: ... def tpc_begin(self, txn, sub=False): ... raise CommitFailure('failed') ... def abort(self, txn): ... pass >>> t = begin() >>> t.join(FailingDataManager()) >>> t.addBeforeCommitHook(hook, ('2',)) >>> from transaction.tests.common import DummyFile >>> from transaction.tests.common import Monkey >>> from transaction.tests.common import assertRaisesEx >>> from transaction import _transaction >>> buffer = DummyFile() >>> with Monkey(_transaction, _TB_BUFFER=buffer): ... err = assertRaisesEx(CommitFailure, t.commit) >>> log ["arg '2' kw1 'no_kw1' kw2 'no_kw2'"] >>> reset_log() Let's register several hooks. .. doctest:: >>> t = begin() >>> t.addBeforeCommitHook(hook, ('4',), dict(kw1='4.1')) >>> t.addBeforeCommitHook(hook, ('5',), dict(kw2='5.2')) They are returned in the same order by getBeforeCommitHooks. .. doctest:: >>> [(hook.__name__, args, kws) #doctest: +NORMALIZE_WHITESPACE ... for hook, args, kws in t.getBeforeCommitHooks()] [('hook', ('4',), {'kw1': '4.1'}), ('hook', ('5',), {'kw2': '5.2'})] And commit also calls them in this order. .. doctest:: >>> t.commit() >>> len(log) 2 >>> log #doctest: +NORMALIZE_WHITESPACE ["arg '4' kw1 '4.1' kw2 'no_kw2'", "arg '5' kw1 'no_kw1' kw2 '5.2'"] >>> reset_log() While executing, a hook can itself add more hooks, and they will all be called before the real commit starts. .. doctest:: >>> def recurse(txn, arg): ... log.append('rec' + str(arg)) ... if arg: ... txn.addBeforeCommitHook(hook, ('-',)) ... txn.addBeforeCommitHook(recurse, (txn, arg-1)) >>> t = begin() >>> t.addBeforeCommitHook(recurse, (t, 3)) >>> commit() >>> log #doctest: +NORMALIZE_WHITESPACE ['rec3', "arg '-' kw1 'no_kw1' kw2 'no_kw2'", 'rec2', "arg '-' kw1 'no_kw1' kw2 'no_kw2'", 'rec1', "arg '-' kw1 'no_kw1' kw2 'no_kw2'", 'rec0'] >>> reset_log() The :meth:`addAfterCommitHook` Method -------------------------------------- Let's define a hook to call, and a way to see that it was called. .. doctest:: >>> log = [] >>> def reset_log(): ... del log[:] >>> def hook(status, arg='no_arg', kw1='no_kw1', kw2='no_kw2'): ... log.append("%r arg %r kw1 %r kw2 %r" % (status, arg, kw1, kw2)) Now register the hook with a transaction. .. doctest:: >>> from transaction import begin >>> t = begin() >>> t.addAfterCommitHook(hook, ('1',)) We can see that the hook is indeed registered. .. doctest:: >>> [(hook.__name__, args, kws) ... for hook, args, kws in t.getAfterCommitHooks()] [('hook', ('1',), {})] When transaction commit is done, the hook is called, with its arguments. .. doctest:: >>> log [] >>> t.commit() >>> log ["True arg '1' kw1 'no_kw1' kw2 'no_kw2'"] >>> reset_log() A hook's registration is consumed whenever the hook is called. Since the hook above was called, it's no longer registered: .. doctest:: >>> from transaction import commit >>> len(list(t.getAfterCommitHooks())) 0 >>> commit() >>> log [] The hook is only called after a full commit, not for a savepoint. .. doctest:: >>> t = begin() >>> t.addAfterCommitHook(hook, ('A',), dict(kw1='B')) >>> dummy = t.savepoint() >>> log [] >>> t.commit() >>> log ["True arg 'A' kw1 'B' kw2 'no_kw2'"] >>> reset_log() If a transaction is aborted, no hook is called. .. doctest:: >>> from transaction import abort >>> t = begin() >>> t.addAfterCommitHook(hook, ["OOPS!"]) >>> abort() >>> log [] >>> commit() >>> log [] The hook is called after the commit is done, so even if the commit fails the hook will have been called. To provoke failures in commit, we'll add failing resource manager to the transaction. .. doctest:: >>> class CommitFailure(Exception): ... pass >>> class FailingDataManager: ... def tpc_begin(self, txn): ... raise CommitFailure('failed') ... def abort(self, txn): ... pass >>> t = begin() >>> t.join(FailingDataManager()) >>> t.addAfterCommitHook(hook, ('2',)) >>> from transaction.tests.common import DummyFile >>> from transaction.tests.common import Monkey >>> from transaction.tests.common import assertRaisesEx >>> from transaction import _transaction >>> buffer = DummyFile() >>> with Monkey(_transaction, _TB_BUFFER=buffer): ... err = assertRaisesEx(CommitFailure, t.commit) >>> log ["False arg '2' kw1 'no_kw1' kw2 'no_kw2'"] >>> reset_log() Let's register several hooks. .. doctest:: >>> t = begin() >>> t.addAfterCommitHook(hook, ('4',), dict(kw1='4.1')) >>> t.addAfterCommitHook(hook, ('5',), dict(kw2='5.2')) They are returned in the same order by getAfterCommitHooks. .. doctest:: >>> [(hook.__name__, args, kws) #doctest: +NORMALIZE_WHITESPACE ... for hook, args, kws in t.getAfterCommitHooks()] [('hook', ('4',), {'kw1': '4.1'}), ('hook', ('5',), {'kw2': '5.2'})] And commit also calls them in this order. .. doctest:: >>> t.commit() >>> len(log) 2 >>> log #doctest: +NORMALIZE_WHITESPACE ["True arg '4' kw1 '4.1' kw2 'no_kw2'", "True arg '5' kw1 'no_kw1' kw2 '5.2'"] >>> reset_log() While executing, a hook can itself add more hooks, and they will all be called before the real commit starts. .. doctest:: >>> def recurse(status, txn, arg): ... log.append('rec' + str(arg)) ... if arg: ... txn.addAfterCommitHook(hook, ('-',)) ... txn.addAfterCommitHook(recurse, (txn, arg-1)) >>> t = begin() >>> t.addAfterCommitHook(recurse, (t, 3)) >>> commit() >>> log #doctest: +NORMALIZE_WHITESPACE ['rec3', "True arg '-' kw1 'no_kw1' kw2 'no_kw2'", 'rec2', "True arg '-' kw1 'no_kw1' kw2 'no_kw2'", 'rec1', "True arg '-' kw1 'no_kw1' kw2 'no_kw2'", 'rec0'] >>> reset_log() If an after commit hook is raising an exception then it will log a message at error level so that if other hooks are registered they can be executed. We don't support execution dependencies at this level. .. doctest:: >>> from transaction import TransactionManager >>> from transaction.tests.test__manager import DataObject >>> mgr = TransactionManager() >>> do = DataObject(mgr) >>> def hookRaise(status, arg='no_arg', kw1='no_kw1', kw2='no_kw2'): ... raise TypeError("Fake raise") >>> t = begin() >>> t.addAfterCommitHook(hook, ('-', 1)) >>> t.addAfterCommitHook(hookRaise, ('-', 2)) >>> t.addAfterCommitHook(hook, ('-', 3)) >>> commit() >>> log ["True arg '-' kw1 1 kw2 'no_kw2'", "True arg '-' kw1 3 kw2 'no_kw2'"] >>> reset_log() Test that the associated transaction manager has been cleaned up when after commit hooks are registered .. doctest:: >>> t = begin() >>> t._manager is not None True >>> t._manager._txn is t True >>> t.addAfterCommitHook(hook, ('-', 1)) >>> commit() >>> log ["True arg '-' kw1 1 kw2 'no_kw2'"] >>> t._manager is None True >>> mgr._txn is None True >>> reset_log() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/_build/html/_sources/index.rst.txt0000644000076500000240000001350614632330173022227 0ustar00jensstaff================================== ``transaction`` Documentation ================================== A general transaction support library for Python. The transaction package offers a two-phase commit protocol which allows multiple backends of any kind to participate in a transaction and commit their changes only if all of them can successfully do so. It also offers support for savepoints, so that part of a transaction can be rolled back without having to abort it completely. There are already transaction backends for SQLAlchemy, ZODB, email, filesystem, and others. in addition, there are packages like pyramid_tm, which allows all 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 =============================== To install the transaction package you can use pip:: $ pip install transaction 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 ================== At its simplest, the developer will use an existing transaction backend, and will at most require to commit or abort a transaction now and then. For example: .. code-block:: python :linenos: import transaction try: # some code that uses one or more backends . . . transaction.commit() except SomeError: transaction.abort() Things you need to know about the transaction machinery ======================================================= .. rubric:: Transactions 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. .. 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 copy. [#wrapped]_ Application developers will most likely never need to create their own transaction 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 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. 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. .. 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: 1. The commit process is started. 2. Each associated data manager prepares the changes to be persistent. 3. Each data manager verifies that no errors or other exceptional conditions occurred during the attempt to persist the changes. If that happens, an exception should be raised. This is called 'voting'. A data manager votes 'no' by raising an exception if something goes wrong; otherwise, its vote is counted as a 'yes'. 4. If any of the associated data managers votes 'no', the transaction is aborted; otherwise, the changes are made permanent. The two phase commit sequence requires that all the storages being used are capable of rolling back or aborting changes. .. rubric:: Savepoints 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. .. [#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 `~.ITransactionManager.unregisterSynch` on the wrapped transaction manager they obtained when created or opened. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/_build/html/_sources/integrations.rst.txt0000644000076500000240000000214214632330173023620 0ustar00jensstaff========================================================= 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, preventing users from receiving notifications about events which may not have completed successfully. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/_build/html/_sources/savepoint.rst.txt0000644000076500000240000002071714632330173023132 0ustar00jensstaffSavepoints ========== Savepoints provide a way to save to disk intermediate work done during a transaction allowing: - partial transaction (subtransaction) rollback (abort) - state of saved objects to be freed, freeing on-line memory for other uses Savepoints make it possible to write atomic subroutines that don't make top-level transaction commitments. Applications ------------ To demonstrate how savepoints work with transactions, we've provided a sample data manager implementation that provides savepoint support. The primary purpose of this data manager is to provide code that can be read to understand how savepoints work. The secondary purpose is to provide support for demonstrating the correct operation of savepoint support within the transaction system. This data manager is very simple. It provides flat storage of named immutable values, like strings and numbers. .. doctest:: >>> import transaction >>> from transaction.tests import savepointsample >>> dm = savepointsample.SampleSavepointDataManager() >>> dm['name'] = 'bob' As with other data managers, we can commit changes: .. doctest:: >>> transaction.commit() >>> dm['name'] 'bob' and abort changes: .. doctest:: >>> dm['name'] = 'sally' >>> dm['name'] 'sally' >>> transaction.abort() >>> dm['name'] 'bob' Now, let's look at an application that manages funds for people. It allows deposits and debits to be entered for multiple people. It accepts a sequence of entries and generates a sequence of status messages. For each entry, it applies the change and then validates the user's account. If the user's account is invalid, we roll back the change for that entry. The success or failure of an entry is indicated in the output status. First we'll initialize some accounts: .. doctest:: >>> dm['bob-balance'] = 0.0 >>> dm['bob-credit'] = 0.0 >>> dm['sally-balance'] = 0.0 >>> dm['sally-credit'] = 100.0 >>> transaction.commit() Now, we'll define a validation function to validate an account: .. doctest:: >>> def validate_account(name): ... if dm[name+'-balance'] + dm[name+'-credit'] < 0: ... raise ValueError('Overdrawn', name) And a function to apply entries. If the function fails in some unexpected way, it rolls back all of its changes and prints the error: .. doctest:: >>> def apply_entries(entries): ... savepoint = transaction.savepoint() ... try: ... for name, amount in entries: ... entry_savepoint = transaction.savepoint() ... try: ... dm[name+'-balance'] += amount ... validate_account(name) ... except ValueError as error: ... entry_savepoint.rollback() ... print("%s %s" % ('Error', str(error))) ... else: ... print("%s %s" % ('Updated', name)) ... except Exception as error: ... savepoint.rollback() ... print("%s" % ('Unexpected exception')) Now let's try applying some entries: .. doctest:: >>> apply_entries([ ... ('bob', 10.0), ... ('sally', 10.0), ... ('bob', 20.0), ... ('sally', 10.0), ... ('bob', -100.0), ... ('sally', -100.0), ... ]) Updated bob Updated sally Updated bob Updated sally Error ('Overdrawn', 'bob') Updated sally >>> dm['bob-balance'] 30.0 >>> dm['sally-balance'] -80.0 If we provide entries that cause an unexpected error: .. doctest:: >>> apply_entries([ ... ('bob', 10.0), ... ('sally', 10.0), ... ('bob', '20.0'), ... ('sally', 10.0), ... ]) Updated bob Updated sally Unexpected exception Because the apply_entries used a savepoint for the entire function, it was able to rollback the partial changes without rolling back changes made in the previous call to ``apply_entries``: .. doctest:: >>> dm['bob-balance'] 30.0 >>> dm['sally-balance'] -80.0 If we now abort the outer transactions, the earlier changes will go away: .. doctest:: >>> transaction.abort() >>> dm['bob-balance'] 0.0 >>> dm['sally-balance'] 0.0 Savepoint invalidation ---------------------- A savepoint can be used any number of times: .. doctest:: >>> dm['bob-balance'] = 100.0 >>> dm['bob-balance'] 100.0 >>> savepoint = transaction.savepoint() >>> dm['bob-balance'] = 200.0 >>> dm['bob-balance'] 200.0 >>> savepoint.rollback() >>> dm['bob-balance'] 100.0 >>> savepoint.rollback() # redundant, but should be harmless >>> dm['bob-balance'] 100.0 >>> dm['bob-balance'] = 300.0 >>> dm['bob-balance'] 300.0 >>> savepoint.rollback() >>> dm['bob-balance'] 100.0 However, using a savepoint invalidates any savepoints that come after it: .. doctest:: >>> dm['bob-balance'] = 200.0 >>> dm['bob-balance'] 200.0 >>> savepoint1 = transaction.savepoint() >>> dm['bob-balance'] = 300.0 >>> dm['bob-balance'] 300.0 >>> savepoint2 = transaction.savepoint() >>> savepoint.rollback() >>> dm['bob-balance'] 100.0 >>> savepoint2.rollback() #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... InvalidSavepointRollbackError: invalidated by a later savepoint >>> savepoint1.rollback() #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... InvalidSavepointRollbackError: invalidated by a later savepoint >>> transaction.abort() Databases without savepoint support ----------------------------------- Normally it's an error to use savepoints with databases that don't support savepoints: .. doctest:: >>> dm_no_sp = savepointsample.SampleDataManager() >>> dm_no_sp['name'] = 'bob' >>> transaction.commit() >>> dm_no_sp['name'] = 'sally' >>> transaction.savepoint() #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TypeError: ('Savepoints unsupported', {'name': 'bob'}) >>> transaction.abort() However, a flag can be passed to the transaction savepoint method to indicate that databases without savepoint support should be tolerated until a savepoint is rolled back. This allows transactions to proceed if there are no reasons to roll back: .. doctest:: >>> dm_no_sp['name'] = 'sally' >>> savepoint = transaction.savepoint(1) >>> dm_no_sp['name'] = 'sue' >>> transaction.commit() >>> dm_no_sp['name'] 'sue' >>> dm_no_sp['name'] = 'sam' >>> savepoint = transaction.savepoint(1) >>> savepoint.rollback() #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TypeError: ('Savepoints unsupported', {'name': 'sam'}) Failures -------- If a failure occurs when creating or rolling back a savepoint, the transaction state will be uncertain and the transaction will become uncommitable. From that point on, most transaction operations, including commit, will fail until the transaction is aborted. In the previous example, we got an error when we tried to rollback the savepoint. If we try to commit the transaction, the commit will fail: .. doctest:: >>> transaction.commit() #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TransactionFailedError: An operation previously failed, with traceback: ... TypeError: ('Savepoints unsupported', {'name': 'sam'}) We have to abort it to make any progress: .. doctest:: >>> transaction.abort() Similarly, in our earlier example, where we tried to take a savepoint with a data manager that didn't support savepoints: .. doctest:: >>> dm_no_sp['name'] = 'sally' >>> dm['name'] = 'sally' >>> savepoint = transaction.savepoint() # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TypeError: ('Savepoints unsupported', {'name': 'sue'}) >>> transaction.commit() # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TransactionFailedError: An operation previously failed, with traceback: ... TypeError: ('Savepoints unsupported', {'name': 'sue'}) >>> transaction.abort() After clearing the transaction with an abort, we can get on with new transactions: .. doctest:: >>> dm_no_sp['name'] = 'sally' >>> dm['name'] = 'sally' >>> transaction.commit() >>> dm_no_sp['name'] 'sally' >>> dm['name'] 'sally' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/_build/html/_sources/sqlalchemy.rst.txt0000644000076500000240000003125614632330173023264 0ustar00jensstaffUsing transactions with SQLAlchemy ================================== Now that we got the terminology out of the way, let's show how to use this package in a Python application. One of the most popular ways of using the transaction package is to combine transactions from the ZODB with a relational database backend. Likewise, one of the most popular ways of communicating with a relational database in Python is to use the SQLAlchemy Object-Relational Mapper. Let's forget about the ZODB for the moment and show how one could use the transaction module in a Python application that needs to talk to a relational database. Installing SQLAlchemy --------------------- Installing SQLAlchemy is as easy as installing any Python package available on PyPi:: $ pip install sqlalchemy This will install the package in your Python environment. You'll need to set up a relational database that you can use to work out the examples in the following sections. SQLAlchemy supports most relational backends that you may have heard of, but the simplest thing to do is to use SQLite, since it doesn't require a separate Python driver. You'll have to make sure that the operating system packages required for using SQLite are present, though. If you want to use another database, make sure you install the required system packages and drivers in addition to the database. For information about which databases are supported and where you can find the drivers, consult http://www.sqlalchemy.org/docs/core/engines.html#supported-dbapis. Choosing a data manager ----------------------- Hopefully, at this point SQLAlchemy and SQLite (or other database if you are feeling adventurous) are installed. To use this combination with the transaction package, we need a data manager that knows how to talk to SQLAlchemy so that the appropriate SQL commands are sent to SQLite whenever an event in the transaction life-cycle occurs. Fortunately for us, there is already a package that does this on PyPI, so it's just a matter of installing it on our system. The package is called zope.sqlalchemy, but despite its name it doesn't depend on any zope packages other than zope.interface. By now you already know how to install it:: $ pip install zope.sqlalchemy You can now create Python applications that use the transaction module to control any SQLAlchemy-supported relational backend. A simple demonstration ---------------------- It's time to show how to use SQLAlchemy together with the transaction package. To avoid lengthy digressions, knowledge of how SQLAlchemy works is assumed. If you are not familiar with that, reading the tutorial at http://www.sqlalchemy.org/docs/orm/tutorial.html will give you a good enough background to understand what follows. After installing the required packages, you may wish to follow along the examples using the Python interpreter where you installed them. The first step is to create an engine: .. code-block:: python :linenos: >>> from sqlalchemy import create_engine >>> engine = create_engine('sqlite:///:memory:') This will connect us to the database. The connection string shown here is for SQLite, if you set up a different database you will need to look up the correct connection string syntax for it. The next step is to define a class that will be mapped to a table in the relational database. SQLAlchemy's declarative syntax allows us to do that easily: .. code-block:: python :linenos: >>> from sqlalchemy import Column, Integer, String >>> from sqlalchemy.ext.declarative import declarative_base >>> Base = declarative_base() >>> class User(Base): >>> __tablename__ = 'users' ... ... id = Column(Integer, primary_key=True) ... name = Column(String) ... fullname = Column(String) ... password = Column(String) ... >>> Base.metadata.create_all(engine) The User class is now mapped to the table named 'users'. The create_all method in line 12 creates the table in case it doesn't exist already. We can now create a session and integrate the zope.sqlalchemy data manager with it so that we can use the transaction machinery. This is done by passing a Session Extension when creating the SQLAlchemy session: .. code-block:: python :linenos: >>> from sqlalchemy.orm import sessionmaker >>> from zope.sqlalchemy import ZopeTransactionExtension >>> Session = sessionmaker(bind=engine, extension=ZopeTransactionExtension()) >>> session = Session() In line 3, we create a session class that is bound to the engine that we set up earlier. Notice how we pass the ZopeTransactionExtension using the extension parameter. This extension connects the SQLAlchemy session with the data manager provided by zope.sqlalchemy. In line 4 we create a session. Under the hood, the ZopeTransactionExtension makes sure that the current transaction is joined by the zope.sqlalchemy data manager, so it's not necessary to explicitly join the transaction in our code. Finally, we are able to put some data inside our new table and commit the transaction: .. code-block:: python :linenos: >>> import transaction >>> session.add(User(id=1, name='John', fullname='John Smith', password='123')) >>> transaction.commit() Since the transaction was already joined by the zope.sqlalchemy data manager, we can just call commit and the transaction is correctly committed. As you can see, the integration between SQLAlchemy and the transaction machinery is pretty transparent. Aborting transactions --------------------- Of course, when using the transaction machinery you can also abort or rollback a transaction. An example follows: .. code-block:: python :linenos: >>> session = Session() >>> john = session.query(User).all()[0] >>> john.fullname u'John Smith' >>> john.fullname = 'John Q. Public' >>> john.fullname u'John Q. Public' >>> transaction.abort() We need a new transaction for this example, so a new session is created. Since the old transaction had ended with the commit, creating a new session joins it to the current transaction, which will be a new one as well. We make a query just to show that our user's fullname is 'John Smith', then we change that to 'John Q. Public'. When the transaction is aborted in line 8, the name is reverted to the old value. If we create a new session and query the table for our old friend John, we'll see that the old value was indeed preserved because of the abort: .. code-block:: python :linenos: >>> session = Session() >>> john = session.query(User).all()[0] >>> john.fullname u'John Smith' Savepoints ---------- A nice feature offered by many transactional backends is the existence of savepoints. These allow in effect to save the changes that we have made at the current point in a transaction, but without committing the transaction. If eventually we need to rollback a future operation, we can use the savepoint to return to the "safe" state that we had saved. Unfortunately not every database supports savepoints and SQLite is precisely one of those that doesn't, which means that in order to be able to test this functionality you will have to install another database, like PostgreSQL. Of course, you can also just take our word that it really works, so suit yourself. Let's see how a savepoint would work using PostgreSQL. First we'll import everything and setup the same table we used in our SQLite examples: .. code-block:: python :linenos: >>> from sqlalchemy import create_engine >>> engine = create_engine('postgresql://postgres@127.0.0.1:5432') >>> from sqlalchemy import Column, Integer, String >>> from sqlalchemy.ext.declarative import declarative_base >>> Base = declarative_base() >>> Base.metadata.create_all(engine) >>> class User(Base): ... __tablename__ = 'users' ... id = Column(Integer, primary_key=True) ... name = Column(String) ... fullname = Column(String) ... password = Column(String) ... >>> Base.metadata.create_all(engine) >>> from sqlalchemy.orm import sessionmaker >>> from zope.sqlalchemy import ZopeTransactionExtension >>> Session = sessionmaker(bind=engine, extension=ZopeTransactionExtension()) We are now ready to create and use a savepoint: .. code-block:: python :linenos: >>> import transaction >>> session = Session() >>> session.add(User(id=1, name='John', fullname='John Smith', password='123')) >>> sp = transaction.savepoint() Everything should look familiar until line 4, where we create a savepoint and assign it to the sp variable. If we never need to rollback, this will not be used, but if course we have to hold on to it in case we do. Now, we'll add a second user: .. code-block:: python :linenos: >>> session.add(User(id=2, name='John', fullname='John Watson', password='123')) >>> [o.fullname for o in session.query(User).all()] [u'John Smith', u'John Watson'] The new user has been added. We have not committed or aborted yet, but suppose we encounter an error condition that requires us to get rid of the new user, but not the one we added first. This is where the savepoint comes handy: .. code-block:: python :linenos: >>> sp.rollback() >>> [o.fullname for o in session.query(User).all()] [u'John Smith'] >>> transaction.commit() As you can see, we just call the rollback method and we are back to where we wanted. The transaction can then be committed and the data that we decided to keep will be saved. Managing more than one backend ============================== Going through the previous section's examples, experienced users of any powerful enough relational backend might have been thinking, "wait, my database already can do that by itself. I can always commit or rollback when I want to, so what's the advantage of using this machinery?" The answer is that if you are using a single backend and it already supports savepoints, you really don't need a transaction manager. The transaction machinery can still be useful with a single backend if it doesn't support transactions. A data manager can be written to add this support. There are existent packages that do this for files stored in a file system or for email sending, just to name a few examples. However, the real power of the transaction manager is the ability to combine two or more of these data managers in a single transaction. Say you need to capture data from a form into a relational database and send email only on transaction commit, that's a good use case for the transaction package. We will illustrate this by showing an example of coordinating transactions to a relational database and a ZODB client. The first thing to do is set up the relational database, using the code that we've seen before: .. code-block:: python :linenos: >>> from sqlalchemy import create_engine >>> engine = create_engine('postgresql://postgres@127.0.0.1:5432') >>> from sqlalchemy import Column, Integer, String >>> from sqlalchemy.ext.declarative import declarative_base >>> Base = declarative_base() >>> Base.metadata.create_all(engine) >>> class User(Base): ... __tablename__ = 'users' ... id = Column(Integer, primary_key=True) ... name = Column(String) ... fullname = Column(String) ... password = Column(String) ... >>> Base.metadata.create_all(engine) >>> from sqlalchemy.orm import sessionmaker >>> from zope.sqlalchemy import ZopeTransactionExtension >>> Session = sessionmaker(bind=engine, extension=ZopeTransactionExtension()) Now, let's set up a ZODB connection (you might need to install the ZODB first): .. code-block:: python :linenos: >>> from ZODB import DB, FileStorage >>> storage = FileStorage.FileStorage('test.fs') >>> db = DB(storage) >>> connection = db.open() >>> root = connection.root() We're ready for adding a user to the relational database table. Right after that, we add some data to the ZODB using the user name as key: .. code-block:: python :linenos: >>> import transaction >>> session.add(User(id=1, name='John', fullname='John Smith', password='123')) >>> root['John'] = 'some data that goes into the object database' Since both the ZopeTransactionExtension and the ZODB connection join the transaction automatically, we can just make the changes we want and be ready to commit the transaction immediately. .. code-block:: python >>> transaction.commit() Again, both the SQLAlchemy and the ZODB data managers joined the transaction, so that we can commit the transaction and both backends save the data. If there's a problem with one of the backends, the transaction is aborted in both regardless of the state of the other. It's also possible to abort the transaction manually, of course, causing a rollback on both backends as well. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1726644280.539638 transaction-5.0/docs/_build/html/_static/0000755000076500000240000000000014672500071017347 5ustar00jensstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/_build/html/_static/placeholder.txt0000644000076500000240000000000014632330173022360 0ustar00jensstaff././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1726644280.5397375 transaction-5.0/docs/_static/0000755000076500000240000000000014672500071015145 5ustar00jensstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/_static/placeholder.txt0000644000076500000240000000000014632330173020156 0ustar00jensstaff././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1726644280.5398579 transaction-5.0/docs/_templates/0000755000076500000240000000000014672500071015654 5ustar00jensstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/_templates/placeholder.txt0000644000076500000240000000000014632330173020665 0ustar00jensstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/api.rst0000644000076500000240000000204714632330173015025 0ustar00jensstaff=============================== ``transaction`` API Reference =============================== Interfaces ========== .. module:: transaction.interfaces .. autointerface:: ITransactionManager .. autointerface:: ITransaction .. autointerface:: IDataManager .. autointerface:: ISavepointDataManager .. autointerface:: IRetryDataManager .. autointerface:: IDataManagerSavepoint .. autointerface:: ISavepoint .. autointerface:: ISynchronizer Exceptions ---------- .. autoclass:: TransactionError .. autoclass:: TransactionFailedError .. autoclass:: DoomedTransaction .. autoclass:: TransientError .. autoclass:: InvalidSavepointRollbackError .. autoclass:: NoTransaction .. autoclass:: AlreadyInTransaction API Objects =========== .. automodule:: transaction .. autoclass:: Transaction .. autoclass:: TransactionManager .. automethod:: __enter__ Alias for :meth:`get` .. automethod:: __exit__ On error, aborts the current transaction. Otherwise, commits. .. autoclass:: ThreadTransactionManager .. autoclass:: Savepoint ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/changes.rst0000644000076500000240000000003414632330173015656 0ustar00jensstaff.. include:: ../CHANGES.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726644116.0 transaction-5.0/docs/conf.py0000644000076500000240000002141314672477624015040 0ustar00jensstaff# # transaction documentation build configuration file, created by # sphinx-quickstart on Wed May 16 16:43:53 2012. # # This file is execfile()d with the current directory set to its containing dir # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys 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 = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx', 'repoze.sphinx.autointerface', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'transaction' copyright = '2012-2024, Zope Foundation Contributors' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '%s.%s' % tuple(map(int, rqmt.version.split('.')[:2])) # The full version, including alpha/beta/rc tags. release = rqmt.version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. default_role = 'obj' # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} html_sidebars = {'**': ['globaltoc.html', 'relations.html', 'sourcelink.html', 'searchbox.html'], } # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'transactiondoc' # -- Options for LaTeX output -------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]) latex_documents = [ ('index', 'transaction.tex', 'transaction Documentation', 'Zope Foundation Contributors', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output -------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'transaction', 'transaction Documentation', ['Zope Foundation Contributors'], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'transaction', 'transaction Documentation', 'Zope Foundation Contributors', 'transaction', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # 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 = { 'python': ('https://docs.python.org/', None), 'zodb': ('http://www.zodb.org/en/latest/', None), } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/convenience.rst0000644000076500000240000000653514632330173016556 0ustar00jensstaff================================= Transaction convenience support ================================= with support ============ We can now use the with statement to define transaction boundaries. .. doctest:: >>> import transaction.tests.savepointsample >>> dm = transaction.tests.savepointsample.SampleSavepointDataManager() >>> list(dm.keys()) [] We can use it with a manager: .. doctest:: >>> with transaction.manager as t: ... dm['z'] = 3 ... t.note(u'test 3') >>> dm['z'] 3 >>> dm.last_note == 'test 3' True >>> with transaction.manager: #doctest ELLIPSIS ... dm['z'] = 4 ... xxx Traceback (most recent call last): ... NameError: ... name 'xxx' is not defined >>> dm['z'] 3 On Python 2, you can also abbreviate ``with transaction.manager:`` as ``with transaction:``. This does not work on Python 3 (see http://bugs.python.org/issue12022). Retries ======= Commits can fail for transient reasons, especially conflicts. Applications will often retry transactions some number of times to overcome transient failures. This typically looks something like:: for i in range(3): try: with transaction.manager: ... some something ... except SomeTransientException: continue else: break This is rather ugly and easy to get wrong. Transaction managers provide two helpers for this case. Running and retrying functions as transactions ---------------------------------------------- The first helper runs a function as a transaction:: def do_somthing(): "Do something" ... some something ... transaction.manager.run(do_somthing) You can also use this as a decorator, which executes the decorated function immediately [#decorator-executes]_:: @transaction.manager.run def _(): "Do something" ... some something ... The transaction manager ``run`` method will run the function and return the results. If the function raises a ``TransientError``, the function will be retried a configurable number of times, 3 by default. Any other exceptions will be raised. The function name (if it isn't ``'_'``) and docstring, if any, are added to the transaction description. You can pass an integer number of times to try to the ``run`` method:: transaction.manager.run(do_somthing, 9) @transaction.manager.run(9) def _(): "Do something" ... some something ... 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:: for attempt in transaction.manager.attempts(): with attempt as t: ... some something ... This runs the code block until it runs without a transient error or until the number of attempts is exceeded. By default, it tries 3 times, but you can pass a number of attempts:: for attempt in transaction.manager.attempts(9): with attempt as t: ... some something ... .. [#decorator-executes] Some people find this easier to read, even though the result isn't a decorated function, but rather the result of calling it in a transaction. The function name ``_`` is used here to emphasize that the function is essentially being used as an anonymous function. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/datamanager.rst0000644000076500000240000002413114632330173016516 0ustar00jensstaff======================== Writing a Data Manager ======================== .. currentmodule:: transaction.interfaces Simple Data Manager =================== .. doctest:: >>> from transaction.tests.examples import DataManager 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:: >>> rm = DataManager() It has two pieces, 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 ``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. The sample data manager updates the state when we call ``tpc_vote``, after calling ``commit``: .. doctest:: >>> t1 = '1' >>> rm.tpc_begin(t1) >>> rm.state, rm.delta (0, 1) >>> rm.commit(t1) >>> rm.tpc_vote(t1) >>> rm.state, rm.delta (1, 1) Now if we call tpc_finish: .. doctest:: >>> 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 data manager 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 >>> 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 ``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 ``tpc_finish`` Method ========================= Complete two-phase commit .. doctest:: >>> rm = DataManager() >>> rm.state 0 >>> rm.inc() We start two-phase commit by calling ``tpc_begin``, ``commit``, and ``tpc_vote``: >>> t1 = '1' >>> 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 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 ``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: ('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 ``abort`` method ==================== The abort method can be called before two-phase commit to throw away work done in the transaction: .. doctest:: >>> dm = DataManager() >>> dm.inc() >>> dm.state, dm.delta (0, 1) >>> t1 = '1' >>> dm.abort(t1) >>> dm.state, dm.delta (0, 0) The abort method also throws away work done in savepoints: .. doctest:: >>> dm.inc() >>> r = dm.savepoint(t1) >>> dm.inc() >>> r = dm.savepoint(t1) >>> dm.state, dm.delta (0, 2) >>> dm.abort(t1) >>> dm.state, dm.delta (0, 0) If savepoints are used, abort must be passed the same transaction: .. doctest:: >>> dm.inc() >>> r = dm.savepoint(t1) >>> t2 = '2' >>> dm.abort(t2) Traceback (most recent call last): ... TypeError: ('Transaction missmatch', '2', '1') >>> dm.abort(t1) 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.tpc_begin(t1) >>> dm.abort(t2) Traceback (most recent call last): ... TypeError: ('Transaction missmatch', '2', '1') >>> dm.abort(t1) The ``commit`` method ===================== Called after ``tpc_begin`` to make changes persistent and prepare for voting. .. doctest:: >>> dm = DataManager() >>> dm.state 0 >>> dm.inc() We start two-phase commit by calling ``tpc_begin`` .. doctest:: >>> t1 = '1' >>> dm.tpc_begin(t1) 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 to call commit without calling ``tpc_begin`` first: .. doctest:: >>> dm = DataManager() >>> t2 = '2' >>> dm.commit(t2) Traceback (most recent call last): ... TypeError: Not prepared to commit If course, the transactions given to ``tpc_begin`` and commit must be the same: .. doctest:: >>> dm = DataManager() >>> t3 = '3' >>> dm.tpc_begin(t3) >>> dm.commit(t2) Traceback (most recent call last): ... TypeError: ('Transaction missmatch', '2', '3') The ``savepoint`` Method ======================== Provide the ability to rollback transaction state Savepoints provide a way to: - Save partial transaction work. For some data managers, this could allow resources to be used more efficiently. - Provide the ability to revert state to a point in a transaction without aborting the entire transaction. In other words, savepoints support partial aborts. Savepoints don't use two-phase commit. If there are errors in setting or rolling back to savepoints, the application should abort the containing transaction. This is *not* the responsibility of the data manager. Savepoints are always associated with a transaction. Any work done in a savepoint's transaction is tentative until the transaction is committed using two-phase commit. .. doctest:: >>> rm = DataManager() >>> 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/developer.rst0000644000076500000240000001204314632330173016236 0ustar00jensstaff================================= ``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 calls the following four methods on each resource manager; it calls `IDataManager.tpc_begin` on each resource manager before calling `IDataManager.commit` on any of them. 1. tpc_begin(txn) 2. commit(txn) 3. tpc_vote(txn) 4. tpc_finish(txn) Before-commit hook ================== Sometimes, applications want to execute some code when a transaction is committed. For example, one might want to delay object indexing until a transaction commits, rather than indexing every time an object is changed. Or someone might want to check invariants only after a set of operations. A pre-commit hook is available for such use cases: use `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 =========== 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 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 and calling ``commit()`` or ``abort()`` implicitly begins a new one. This behavior can be convenient for interactive use, but invites subtle bugs: - Calling begin() without realizing that there are outstanding changes that will be aborted. - Interacting with a database without controlling transactions, in which case changes may be unexpectedly discarded. For applications, including frameworks that control transactions, transaction managers provide an optional explicit mode. Transaction managers have an ``explicit`` constructor keyword argument that, if True puts the transaction manager in explicit mode. In explicit mode: - It is an error to call ``get()``, ``commit()``, ``abort()``, ``doom()``, ``isDoomed``, or ``savepoint()`` without a preceding ``begin()`` call. Doing so will raise a ``NoTransaction`` exception. - It is an error to call ``begin()`` after a previous ``begin()`` without an intervening ``commit()`` or ``abort()`` call. Doing so will raise an ``AlreadyInTransaction`` exception. In explicit mode, bugs like those mentioned above are much easier to avoid because they cause explicit exceptions that can typically be caught in development. An additional benefit of explicit mode is that it can allow data managers to manage resources more efficiently. Transaction managers have an explicit attribute that can be queried to determine if explicit mode is enabled. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/doom.rst0000644000076500000240000001050214632330173015205 0ustar00jensstaffDooming Transactions ==================== A doomed transaction behaves exactly the same way as an active transaction but raises an error on any attempt to commit it, thus forcing an abort. Doom is useful in places where abort is unsafe and an exception cannot be raised. This occurs when the programmer wants the code following the doom to run but not commit. It is unsafe to abort in these circumstances as a following get() may implicitly open a new transaction. Any attempt to commit a doomed transaction will raise a DoomedTransaction exception. An example of such a use case can be found in zope/app/form/browser/editview.py. Here a form validation failure must doom the transaction as committing the transaction may have side-effects. However, the form code must continue to calculate a form containing the error messages to return. For Zope in general, code running within a request should always doom transactions rather than aborting them. It is the responsibilty of the publication to either abort() or commit() the transaction. Application code can use savepoints and doom() safely. To see how it works we first need to create a stub data manager: .. doctest:: >>> from transaction.interfaces import IDataManager >>> from zope.interface import implementer >>> @implementer(IDataManager) ... class DataManager: ... def __init__(self): ... self.attr_counter = {} ... def __getattr__(self, name): ... def f(transaction): ... self.attr_counter[name] = self.attr_counter.get(name, 0) + 1 ... return f ... def total(self): ... count = 0 ... for access_count in self.attr_counter.values(): ... count += access_count ... return count ... def sortKey(self): ... return '1' Start a new transaction: .. doctest:: >>> import transaction >>> txn = transaction.begin() >>> dm = DataManager() >>> txn.join(dm) We can ask a transaction if it is doomed to avoid expensive operations. An example of a use case is an object-relational mapper where a pre-commit hook sends all outstanding SQL to a relational database for objects changed during the transaction. This expensive operation is not necessary if the transaction has been doomed. A non-doomed transaction should return False: .. doctest:: >>> txn.isDoomed() False We can doom a transaction by calling .doom() on it: .. doctest:: >>> txn.doom() >>> txn.isDoomed() True We can doom it again if we like: .. doctest:: >>> txn.doom() The data manager is unchanged at this point: .. doctest:: >>> dm.total() 0 Attempting to commit a doomed transaction any number of times raises a DoomedTransaction: .. doctest:: >>> txn.commit() # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): DoomedTransaction: transaction doomed, cannot commit >>> txn.commit() # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): DoomedTransaction: transaction doomed, cannot commit But still leaves the data manager unchanged: .. doctest:: >>> dm.total() 0 But the doomed transaction can be aborted: .. doctest:: >>> txn.abort() Which aborts the data manager: .. doctest:: >>> dm.total() 1 >>> dm.attr_counter['abort'] 1 Dooming the current transaction can also be done directly from the transaction module. We can also begin a new transaction directly after dooming the old one: .. doctest:: >>> txn = transaction.begin() >>> transaction.isDoomed() False >>> transaction.doom() >>> transaction.isDoomed() True >>> txn = transaction.begin() After committing a transaction we get an assertion error if we try to doom the transaction. This could be made more specific, but trying to doom a transaction after it's been committed is probably a programming error: .. doctest:: >>> txn = transaction.begin() >>> txn.commit() >>> txn.doom() Traceback (most recent call last): ... ValueError: non-doomable A doomed transaction should act the same as an active transaction, so we should be able to join it: .. doctest:: >>> txn = transaction.begin() >>> txn.doom() >>> dm2 = DataManager() >>> txn.join(dm2) Clean up: .. doctest:: >>> txn = transaction.begin() >>> txn.abort() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/hooks.rst0000644000076500000240000002352114632330173015377 0ustar00jensstaffHooking the Transaction Machinery ================================= The :mod:`transaction` machinery allows application developers to register two different groups of callbacks to be called, one group before committing the transaction and one group after. These hooks are **not** designed to be used as replacements for the two-phase commit machinery defined by a resource manager (see :doc:`resourcemanager`). In particular, hook functions **must not** raise or propagate exceptions. .. warning:: Hook functions which *do* raise or propagate exceptions will leave the application in an undefined state. The :meth:`addBeforeCommitHook` Method -------------------------------------- Let's define a hook to call, and a way to see that it was called. .. doctest:: >>> log = [] >>> def reset_log(): ... del log[:] >>> def hook(arg='no_arg', kw1='no_kw1', kw2='no_kw2'): ... log.append("arg %r kw1 %r kw2 %r" % (arg, kw1, kw2)) Now register the hook with a transaction. .. doctest:: >>> from transaction import begin >>> import transaction >>> t = begin() >>> t.addBeforeCommitHook(hook, ('1',)) We can see that the hook is indeed registered. .. doctest:: >>> [(hook.__name__, args, kws) ... for hook, args, kws in t.getBeforeCommitHooks()] [('hook', ('1',), {})] When transaction commit starts, the hook is called, with its arguments. .. doctest:: >>> log [] >>> t.commit() >>> log ["arg '1' kw1 'no_kw1' kw2 'no_kw2'"] >>> reset_log() A hook's registration is consumed whenever the hook is called. Since the hook above was called, it's no longer registered: .. doctest:: >>> from transaction import commit >>> len(list(t.getBeforeCommitHooks())) 0 >>> commit() >>> log [] The hook is only called for a full commit, not for a savepoint. .. doctest:: >>> t = begin() >>> t.addBeforeCommitHook(hook, ('A',), dict(kw1='B')) >>> dummy = t.savepoint() >>> log [] >>> t.commit() >>> log ["arg 'A' kw1 'B' kw2 'no_kw2'"] >>> reset_log() If a transaction is aborted, no hook is called. .. doctest:: >>> from transaction import abort >>> t = begin() >>> t.addBeforeCommitHook(hook, ["OOPS!"]) >>> abort() >>> log [] >>> commit() >>> log [] The hook is called before the commit does anything, so even if the commit fails the hook will have been called. To provoke failures in commit, we'll add failing resource manager to the transaction. .. doctest:: >>> class CommitFailure(Exception): ... pass >>> class FailingDataManager: ... def tpc_begin(self, txn, sub=False): ... raise CommitFailure('failed') ... def abort(self, txn): ... pass >>> t = begin() >>> t.join(FailingDataManager()) >>> t.addBeforeCommitHook(hook, ('2',)) >>> from transaction.tests.common import DummyFile >>> from transaction.tests.common import Monkey >>> from transaction.tests.common import assertRaisesEx >>> from transaction import _transaction >>> buffer = DummyFile() >>> with Monkey(_transaction, _TB_BUFFER=buffer): ... err = assertRaisesEx(CommitFailure, t.commit) >>> log ["arg '2' kw1 'no_kw1' kw2 'no_kw2'"] >>> reset_log() Let's register several hooks. .. doctest:: >>> t = begin() >>> t.addBeforeCommitHook(hook, ('4',), dict(kw1='4.1')) >>> t.addBeforeCommitHook(hook, ('5',), dict(kw2='5.2')) They are returned in the same order by getBeforeCommitHooks. .. doctest:: >>> [(hook.__name__, args, kws) #doctest: +NORMALIZE_WHITESPACE ... for hook, args, kws in t.getBeforeCommitHooks()] [('hook', ('4',), {'kw1': '4.1'}), ('hook', ('5',), {'kw2': '5.2'})] And commit also calls them in this order. .. doctest:: >>> t.commit() >>> len(log) 2 >>> log #doctest: +NORMALIZE_WHITESPACE ["arg '4' kw1 '4.1' kw2 'no_kw2'", "arg '5' kw1 'no_kw1' kw2 '5.2'"] >>> reset_log() While executing, a hook can itself add more hooks, and they will all be called before the real commit starts. .. doctest:: >>> def recurse(txn, arg): ... log.append('rec' + str(arg)) ... if arg: ... txn.addBeforeCommitHook(hook, ('-',)) ... txn.addBeforeCommitHook(recurse, (txn, arg-1)) >>> t = begin() >>> t.addBeforeCommitHook(recurse, (t, 3)) >>> commit() >>> log #doctest: +NORMALIZE_WHITESPACE ['rec3', "arg '-' kw1 'no_kw1' kw2 'no_kw2'", 'rec2', "arg '-' kw1 'no_kw1' kw2 'no_kw2'", 'rec1', "arg '-' kw1 'no_kw1' kw2 'no_kw2'", 'rec0'] >>> reset_log() The :meth:`addAfterCommitHook` Method -------------------------------------- Let's define a hook to call, and a way to see that it was called. .. doctest:: >>> log = [] >>> def reset_log(): ... del log[:] >>> def hook(status, arg='no_arg', kw1='no_kw1', kw2='no_kw2'): ... log.append("%r arg %r kw1 %r kw2 %r" % (status, arg, kw1, kw2)) Now register the hook with a transaction. .. doctest:: >>> from transaction import begin >>> t = begin() >>> t.addAfterCommitHook(hook, ('1',)) We can see that the hook is indeed registered. .. doctest:: >>> [(hook.__name__, args, kws) ... for hook, args, kws in t.getAfterCommitHooks()] [('hook', ('1',), {})] When transaction commit is done, the hook is called, with its arguments. .. doctest:: >>> log [] >>> t.commit() >>> log ["True arg '1' kw1 'no_kw1' kw2 'no_kw2'"] >>> reset_log() A hook's registration is consumed whenever the hook is called. Since the hook above was called, it's no longer registered: .. doctest:: >>> from transaction import commit >>> len(list(t.getAfterCommitHooks())) 0 >>> commit() >>> log [] The hook is only called after a full commit, not for a savepoint. .. doctest:: >>> t = begin() >>> t.addAfterCommitHook(hook, ('A',), dict(kw1='B')) >>> dummy = t.savepoint() >>> log [] >>> t.commit() >>> log ["True arg 'A' kw1 'B' kw2 'no_kw2'"] >>> reset_log() If a transaction is aborted, no hook is called. .. doctest:: >>> from transaction import abort >>> t = begin() >>> t.addAfterCommitHook(hook, ["OOPS!"]) >>> abort() >>> log [] >>> commit() >>> log [] The hook is called after the commit is done, so even if the commit fails the hook will have been called. To provoke failures in commit, we'll add failing resource manager to the transaction. .. doctest:: >>> class CommitFailure(Exception): ... pass >>> class FailingDataManager: ... def tpc_begin(self, txn): ... raise CommitFailure('failed') ... def abort(self, txn): ... pass >>> t = begin() >>> t.join(FailingDataManager()) >>> t.addAfterCommitHook(hook, ('2',)) >>> from transaction.tests.common import DummyFile >>> from transaction.tests.common import Monkey >>> from transaction.tests.common import assertRaisesEx >>> from transaction import _transaction >>> buffer = DummyFile() >>> with Monkey(_transaction, _TB_BUFFER=buffer): ... err = assertRaisesEx(CommitFailure, t.commit) >>> log ["False arg '2' kw1 'no_kw1' kw2 'no_kw2'"] >>> reset_log() Let's register several hooks. .. doctest:: >>> t = begin() >>> t.addAfterCommitHook(hook, ('4',), dict(kw1='4.1')) >>> t.addAfterCommitHook(hook, ('5',), dict(kw2='5.2')) They are returned in the same order by getAfterCommitHooks. .. doctest:: >>> [(hook.__name__, args, kws) #doctest: +NORMALIZE_WHITESPACE ... for hook, args, kws in t.getAfterCommitHooks()] [('hook', ('4',), {'kw1': '4.1'}), ('hook', ('5',), {'kw2': '5.2'})] And commit also calls them in this order. .. doctest:: >>> t.commit() >>> len(log) 2 >>> log #doctest: +NORMALIZE_WHITESPACE ["True arg '4' kw1 '4.1' kw2 'no_kw2'", "True arg '5' kw1 'no_kw1' kw2 '5.2'"] >>> reset_log() While executing, a hook can itself add more hooks, and they will all be called before the real commit starts. .. doctest:: >>> def recurse(status, txn, arg): ... log.append('rec' + str(arg)) ... if arg: ... txn.addAfterCommitHook(hook, ('-',)) ... txn.addAfterCommitHook(recurse, (txn, arg-1)) >>> t = begin() >>> t.addAfterCommitHook(recurse, (t, 3)) >>> commit() >>> log #doctest: +NORMALIZE_WHITESPACE ['rec3', "True arg '-' kw1 'no_kw1' kw2 'no_kw2'", 'rec2', "True arg '-' kw1 'no_kw1' kw2 'no_kw2'", 'rec1', "True arg '-' kw1 'no_kw1' kw2 'no_kw2'", 'rec0'] >>> reset_log() If an after commit hook is raising an exception then it will log a message at error level so that if other hooks are registered they can be executed. We don't support execution dependencies at this level. .. doctest:: >>> from transaction import TransactionManager >>> from transaction.tests.test__manager import DataObject >>> mgr = TransactionManager() >>> do = DataObject(mgr) >>> def hookRaise(status, arg='no_arg', kw1='no_kw1', kw2='no_kw2'): ... raise TypeError("Fake raise") >>> t = begin() >>> t.addAfterCommitHook(hook, ('-', 1)) >>> t.addAfterCommitHook(hookRaise, ('-', 2)) >>> t.addAfterCommitHook(hook, ('-', 3)) >>> commit() >>> log ["True arg '-' kw1 1 kw2 'no_kw2'", "True arg '-' kw1 3 kw2 'no_kw2'"] >>> reset_log() Test that the associated transaction manager has been cleaned up when after commit hooks are registered .. doctest:: >>> t = begin() >>> t._manager is not None True >>> t._manager._txn is t True >>> t.addAfterCommitHook(hook, ('-', 1)) >>> commit() >>> log ["True arg '-' kw1 1 kw2 'no_kw2'"] >>> t._manager is None True >>> mgr._txn is None True >>> reset_log() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/index.rst0000644000076500000240000001350614632330173015365 0ustar00jensstaff================================== ``transaction`` Documentation ================================== A general transaction support library for Python. The transaction package offers a two-phase commit protocol which allows multiple backends of any kind to participate in a transaction and commit their changes only if all of them can successfully do so. It also offers support for savepoints, so that part of a transaction can be rolled back without having to abort it completely. There are already transaction backends for SQLAlchemy, ZODB, email, filesystem, and others. in addition, there are packages like pyramid_tm, which allows all 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 =============================== To install the transaction package you can use pip:: $ pip install transaction 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 ================== At its simplest, the developer will use an existing transaction backend, and will at most require to commit or abort a transaction now and then. For example: .. code-block:: python :linenos: import transaction try: # some code that uses one or more backends . . . transaction.commit() except SomeError: transaction.abort() Things you need to know about the transaction machinery ======================================================= .. rubric:: Transactions 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. .. 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 copy. [#wrapped]_ Application developers will most likely never need to create their own transaction 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 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. 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. .. 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: 1. The commit process is started. 2. Each associated data manager prepares the changes to be persistent. 3. Each data manager verifies that no errors or other exceptional conditions occurred during the attempt to persist the changes. If that happens, an exception should be raised. This is called 'voting'. A data manager votes 'no' by raising an exception if something goes wrong; otherwise, its vote is counted as a 'yes'. 4. If any of the associated data managers votes 'no', the transaction is aborted; otherwise, the changes are made permanent. The two phase commit sequence requires that all the storages being used are capable of rolling back or aborting changes. .. rubric:: Savepoints 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. .. [#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 `~.ITransactionManager.unregisterSynch` on the wrapped transaction manager they obtained when created or opened. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/integrations.rst0000644000076500000240000000214214632330173016756 0ustar00jensstaff========================================================= 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, preventing users from receiving notifications about events which may not have completed successfully. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/make.bat0000644000076500000240000001176214632330173015133 0ustar00jensstaff@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\transaction.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\transaction.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/requirements.txt0000644000076500000240000000004314632330173017000 0ustar00jensstaffSphinx repoze.sphinx.autointerface ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/savepoint.rst0000644000076500000240000002071714632330173016270 0ustar00jensstaffSavepoints ========== Savepoints provide a way to save to disk intermediate work done during a transaction allowing: - partial transaction (subtransaction) rollback (abort) - state of saved objects to be freed, freeing on-line memory for other uses Savepoints make it possible to write atomic subroutines that don't make top-level transaction commitments. Applications ------------ To demonstrate how savepoints work with transactions, we've provided a sample data manager implementation that provides savepoint support. The primary purpose of this data manager is to provide code that can be read to understand how savepoints work. The secondary purpose is to provide support for demonstrating the correct operation of savepoint support within the transaction system. This data manager is very simple. It provides flat storage of named immutable values, like strings and numbers. .. doctest:: >>> import transaction >>> from transaction.tests import savepointsample >>> dm = savepointsample.SampleSavepointDataManager() >>> dm['name'] = 'bob' As with other data managers, we can commit changes: .. doctest:: >>> transaction.commit() >>> dm['name'] 'bob' and abort changes: .. doctest:: >>> dm['name'] = 'sally' >>> dm['name'] 'sally' >>> transaction.abort() >>> dm['name'] 'bob' Now, let's look at an application that manages funds for people. It allows deposits and debits to be entered for multiple people. It accepts a sequence of entries and generates a sequence of status messages. For each entry, it applies the change and then validates the user's account. If the user's account is invalid, we roll back the change for that entry. The success or failure of an entry is indicated in the output status. First we'll initialize some accounts: .. doctest:: >>> dm['bob-balance'] = 0.0 >>> dm['bob-credit'] = 0.0 >>> dm['sally-balance'] = 0.0 >>> dm['sally-credit'] = 100.0 >>> transaction.commit() Now, we'll define a validation function to validate an account: .. doctest:: >>> def validate_account(name): ... if dm[name+'-balance'] + dm[name+'-credit'] < 0: ... raise ValueError('Overdrawn', name) And a function to apply entries. If the function fails in some unexpected way, it rolls back all of its changes and prints the error: .. doctest:: >>> def apply_entries(entries): ... savepoint = transaction.savepoint() ... try: ... for name, amount in entries: ... entry_savepoint = transaction.savepoint() ... try: ... dm[name+'-balance'] += amount ... validate_account(name) ... except ValueError as error: ... entry_savepoint.rollback() ... print("%s %s" % ('Error', str(error))) ... else: ... print("%s %s" % ('Updated', name)) ... except Exception as error: ... savepoint.rollback() ... print("%s" % ('Unexpected exception')) Now let's try applying some entries: .. doctest:: >>> apply_entries([ ... ('bob', 10.0), ... ('sally', 10.0), ... ('bob', 20.0), ... ('sally', 10.0), ... ('bob', -100.0), ... ('sally', -100.0), ... ]) Updated bob Updated sally Updated bob Updated sally Error ('Overdrawn', 'bob') Updated sally >>> dm['bob-balance'] 30.0 >>> dm['sally-balance'] -80.0 If we provide entries that cause an unexpected error: .. doctest:: >>> apply_entries([ ... ('bob', 10.0), ... ('sally', 10.0), ... ('bob', '20.0'), ... ('sally', 10.0), ... ]) Updated bob Updated sally Unexpected exception Because the apply_entries used a savepoint for the entire function, it was able to rollback the partial changes without rolling back changes made in the previous call to ``apply_entries``: .. doctest:: >>> dm['bob-balance'] 30.0 >>> dm['sally-balance'] -80.0 If we now abort the outer transactions, the earlier changes will go away: .. doctest:: >>> transaction.abort() >>> dm['bob-balance'] 0.0 >>> dm['sally-balance'] 0.0 Savepoint invalidation ---------------------- A savepoint can be used any number of times: .. doctest:: >>> dm['bob-balance'] = 100.0 >>> dm['bob-balance'] 100.0 >>> savepoint = transaction.savepoint() >>> dm['bob-balance'] = 200.0 >>> dm['bob-balance'] 200.0 >>> savepoint.rollback() >>> dm['bob-balance'] 100.0 >>> savepoint.rollback() # redundant, but should be harmless >>> dm['bob-balance'] 100.0 >>> dm['bob-balance'] = 300.0 >>> dm['bob-balance'] 300.0 >>> savepoint.rollback() >>> dm['bob-balance'] 100.0 However, using a savepoint invalidates any savepoints that come after it: .. doctest:: >>> dm['bob-balance'] = 200.0 >>> dm['bob-balance'] 200.0 >>> savepoint1 = transaction.savepoint() >>> dm['bob-balance'] = 300.0 >>> dm['bob-balance'] 300.0 >>> savepoint2 = transaction.savepoint() >>> savepoint.rollback() >>> dm['bob-balance'] 100.0 >>> savepoint2.rollback() #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... InvalidSavepointRollbackError: invalidated by a later savepoint >>> savepoint1.rollback() #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... InvalidSavepointRollbackError: invalidated by a later savepoint >>> transaction.abort() Databases without savepoint support ----------------------------------- Normally it's an error to use savepoints with databases that don't support savepoints: .. doctest:: >>> dm_no_sp = savepointsample.SampleDataManager() >>> dm_no_sp['name'] = 'bob' >>> transaction.commit() >>> dm_no_sp['name'] = 'sally' >>> transaction.savepoint() #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TypeError: ('Savepoints unsupported', {'name': 'bob'}) >>> transaction.abort() However, a flag can be passed to the transaction savepoint method to indicate that databases without savepoint support should be tolerated until a savepoint is rolled back. This allows transactions to proceed if there are no reasons to roll back: .. doctest:: >>> dm_no_sp['name'] = 'sally' >>> savepoint = transaction.savepoint(1) >>> dm_no_sp['name'] = 'sue' >>> transaction.commit() >>> dm_no_sp['name'] 'sue' >>> dm_no_sp['name'] = 'sam' >>> savepoint = transaction.savepoint(1) >>> savepoint.rollback() #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TypeError: ('Savepoints unsupported', {'name': 'sam'}) Failures -------- If a failure occurs when creating or rolling back a savepoint, the transaction state will be uncertain and the transaction will become uncommitable. From that point on, most transaction operations, including commit, will fail until the transaction is aborted. In the previous example, we got an error when we tried to rollback the savepoint. If we try to commit the transaction, the commit will fail: .. doctest:: >>> transaction.commit() #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TransactionFailedError: An operation previously failed, with traceback: ... TypeError: ('Savepoints unsupported', {'name': 'sam'}) We have to abort it to make any progress: .. doctest:: >>> transaction.abort() Similarly, in our earlier example, where we tried to take a savepoint with a data manager that didn't support savepoints: .. doctest:: >>> dm_no_sp['name'] = 'sally' >>> dm['name'] = 'sally' >>> savepoint = transaction.savepoint() # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TypeError: ('Savepoints unsupported', {'name': 'sue'}) >>> transaction.commit() # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TransactionFailedError: An operation previously failed, with traceback: ... TypeError: ('Savepoints unsupported', {'name': 'sue'}) >>> transaction.abort() After clearing the transaction with an abort, we can get on with new transactions: .. doctest:: >>> dm_no_sp['name'] = 'sally' >>> dm['name'] = 'sally' >>> transaction.commit() >>> dm_no_sp['name'] 'sally' >>> dm['name'] 'sally' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/docs/sqlalchemy.rst0000644000076500000240000003125614632330173016422 0ustar00jensstaffUsing transactions with SQLAlchemy ================================== Now that we got the terminology out of the way, let's show how to use this package in a Python application. One of the most popular ways of using the transaction package is to combine transactions from the ZODB with a relational database backend. Likewise, one of the most popular ways of communicating with a relational database in Python is to use the SQLAlchemy Object-Relational Mapper. Let's forget about the ZODB for the moment and show how one could use the transaction module in a Python application that needs to talk to a relational database. Installing SQLAlchemy --------------------- Installing SQLAlchemy is as easy as installing any Python package available on PyPi:: $ pip install sqlalchemy This will install the package in your Python environment. You'll need to set up a relational database that you can use to work out the examples in the following sections. SQLAlchemy supports most relational backends that you may have heard of, but the simplest thing to do is to use SQLite, since it doesn't require a separate Python driver. You'll have to make sure that the operating system packages required for using SQLite are present, though. If you want to use another database, make sure you install the required system packages and drivers in addition to the database. For information about which databases are supported and where you can find the drivers, consult http://www.sqlalchemy.org/docs/core/engines.html#supported-dbapis. Choosing a data manager ----------------------- Hopefully, at this point SQLAlchemy and SQLite (or other database if you are feeling adventurous) are installed. To use this combination with the transaction package, we need a data manager that knows how to talk to SQLAlchemy so that the appropriate SQL commands are sent to SQLite whenever an event in the transaction life-cycle occurs. Fortunately for us, there is already a package that does this on PyPI, so it's just a matter of installing it on our system. The package is called zope.sqlalchemy, but despite its name it doesn't depend on any zope packages other than zope.interface. By now you already know how to install it:: $ pip install zope.sqlalchemy You can now create Python applications that use the transaction module to control any SQLAlchemy-supported relational backend. A simple demonstration ---------------------- It's time to show how to use SQLAlchemy together with the transaction package. To avoid lengthy digressions, knowledge of how SQLAlchemy works is assumed. If you are not familiar with that, reading the tutorial at http://www.sqlalchemy.org/docs/orm/tutorial.html will give you a good enough background to understand what follows. After installing the required packages, you may wish to follow along the examples using the Python interpreter where you installed them. The first step is to create an engine: .. code-block:: python :linenos: >>> from sqlalchemy import create_engine >>> engine = create_engine('sqlite:///:memory:') This will connect us to the database. The connection string shown here is for SQLite, if you set up a different database you will need to look up the correct connection string syntax for it. The next step is to define a class that will be mapped to a table in the relational database. SQLAlchemy's declarative syntax allows us to do that easily: .. code-block:: python :linenos: >>> from sqlalchemy import Column, Integer, String >>> from sqlalchemy.ext.declarative import declarative_base >>> Base = declarative_base() >>> class User(Base): >>> __tablename__ = 'users' ... ... id = Column(Integer, primary_key=True) ... name = Column(String) ... fullname = Column(String) ... password = Column(String) ... >>> Base.metadata.create_all(engine) The User class is now mapped to the table named 'users'. The create_all method in line 12 creates the table in case it doesn't exist already. We can now create a session and integrate the zope.sqlalchemy data manager with it so that we can use the transaction machinery. This is done by passing a Session Extension when creating the SQLAlchemy session: .. code-block:: python :linenos: >>> from sqlalchemy.orm import sessionmaker >>> from zope.sqlalchemy import ZopeTransactionExtension >>> Session = sessionmaker(bind=engine, extension=ZopeTransactionExtension()) >>> session = Session() In line 3, we create a session class that is bound to the engine that we set up earlier. Notice how we pass the ZopeTransactionExtension using the extension parameter. This extension connects the SQLAlchemy session with the data manager provided by zope.sqlalchemy. In line 4 we create a session. Under the hood, the ZopeTransactionExtension makes sure that the current transaction is joined by the zope.sqlalchemy data manager, so it's not necessary to explicitly join the transaction in our code. Finally, we are able to put some data inside our new table and commit the transaction: .. code-block:: python :linenos: >>> import transaction >>> session.add(User(id=1, name='John', fullname='John Smith', password='123')) >>> transaction.commit() Since the transaction was already joined by the zope.sqlalchemy data manager, we can just call commit and the transaction is correctly committed. As you can see, the integration between SQLAlchemy and the transaction machinery is pretty transparent. Aborting transactions --------------------- Of course, when using the transaction machinery you can also abort or rollback a transaction. An example follows: .. code-block:: python :linenos: >>> session = Session() >>> john = session.query(User).all()[0] >>> john.fullname u'John Smith' >>> john.fullname = 'John Q. Public' >>> john.fullname u'John Q. Public' >>> transaction.abort() We need a new transaction for this example, so a new session is created. Since the old transaction had ended with the commit, creating a new session joins it to the current transaction, which will be a new one as well. We make a query just to show that our user's fullname is 'John Smith', then we change that to 'John Q. Public'. When the transaction is aborted in line 8, the name is reverted to the old value. If we create a new session and query the table for our old friend John, we'll see that the old value was indeed preserved because of the abort: .. code-block:: python :linenos: >>> session = Session() >>> john = session.query(User).all()[0] >>> john.fullname u'John Smith' Savepoints ---------- A nice feature offered by many transactional backends is the existence of savepoints. These allow in effect to save the changes that we have made at the current point in a transaction, but without committing the transaction. If eventually we need to rollback a future operation, we can use the savepoint to return to the "safe" state that we had saved. Unfortunately not every database supports savepoints and SQLite is precisely one of those that doesn't, which means that in order to be able to test this functionality you will have to install another database, like PostgreSQL. Of course, you can also just take our word that it really works, so suit yourself. Let's see how a savepoint would work using PostgreSQL. First we'll import everything and setup the same table we used in our SQLite examples: .. code-block:: python :linenos: >>> from sqlalchemy import create_engine >>> engine = create_engine('postgresql://postgres@127.0.0.1:5432') >>> from sqlalchemy import Column, Integer, String >>> from sqlalchemy.ext.declarative import declarative_base >>> Base = declarative_base() >>> Base.metadata.create_all(engine) >>> class User(Base): ... __tablename__ = 'users' ... id = Column(Integer, primary_key=True) ... name = Column(String) ... fullname = Column(String) ... password = Column(String) ... >>> Base.metadata.create_all(engine) >>> from sqlalchemy.orm import sessionmaker >>> from zope.sqlalchemy import ZopeTransactionExtension >>> Session = sessionmaker(bind=engine, extension=ZopeTransactionExtension()) We are now ready to create and use a savepoint: .. code-block:: python :linenos: >>> import transaction >>> session = Session() >>> session.add(User(id=1, name='John', fullname='John Smith', password='123')) >>> sp = transaction.savepoint() Everything should look familiar until line 4, where we create a savepoint and assign it to the sp variable. If we never need to rollback, this will not be used, but if course we have to hold on to it in case we do. Now, we'll add a second user: .. code-block:: python :linenos: >>> session.add(User(id=2, name='John', fullname='John Watson', password='123')) >>> [o.fullname for o in session.query(User).all()] [u'John Smith', u'John Watson'] The new user has been added. We have not committed or aborted yet, but suppose we encounter an error condition that requires us to get rid of the new user, but not the one we added first. This is where the savepoint comes handy: .. code-block:: python :linenos: >>> sp.rollback() >>> [o.fullname for o in session.query(User).all()] [u'John Smith'] >>> transaction.commit() As you can see, we just call the rollback method and we are back to where we wanted. The transaction can then be committed and the data that we decided to keep will be saved. Managing more than one backend ============================== Going through the previous section's examples, experienced users of any powerful enough relational backend might have been thinking, "wait, my database already can do that by itself. I can always commit or rollback when I want to, so what's the advantage of using this machinery?" The answer is that if you are using a single backend and it already supports savepoints, you really don't need a transaction manager. The transaction machinery can still be useful with a single backend if it doesn't support transactions. A data manager can be written to add this support. There are existent packages that do this for files stored in a file system or for email sending, just to name a few examples. However, the real power of the transaction manager is the ability to combine two or more of these data managers in a single transaction. Say you need to capture data from a form into a relational database and send email only on transaction commit, that's a good use case for the transaction package. We will illustrate this by showing an example of coordinating transactions to a relational database and a ZODB client. The first thing to do is set up the relational database, using the code that we've seen before: .. code-block:: python :linenos: >>> from sqlalchemy import create_engine >>> engine = create_engine('postgresql://postgres@127.0.0.1:5432') >>> from sqlalchemy import Column, Integer, String >>> from sqlalchemy.ext.declarative import declarative_base >>> Base = declarative_base() >>> Base.metadata.create_all(engine) >>> class User(Base): ... __tablename__ = 'users' ... id = Column(Integer, primary_key=True) ... name = Column(String) ... fullname = Column(String) ... password = Column(String) ... >>> Base.metadata.create_all(engine) >>> from sqlalchemy.orm import sessionmaker >>> from zope.sqlalchemy import ZopeTransactionExtension >>> Session = sessionmaker(bind=engine, extension=ZopeTransactionExtension()) Now, let's set up a ZODB connection (you might need to install the ZODB first): .. code-block:: python :linenos: >>> from ZODB import DB, FileStorage >>> storage = FileStorage.FileStorage('test.fs') >>> db = DB(storage) >>> connection = db.open() >>> root = connection.root() We're ready for adding a user to the relational database table. Right after that, we add some data to the ZODB using the user name as key: .. code-block:: python :linenos: >>> import transaction >>> session.add(User(id=1, name='John', fullname='John Smith', password='123')) >>> root['John'] = 'some data that goes into the object database' Since both the ZopeTransactionExtension and the ZODB connection join the transaction automatically, we can just make the changes we want and be ready to commit the transaction immediately. .. code-block:: python >>> transaction.commit() Again, both the SQLAlchemy and the ZODB data managers joined the transaction, so that we can commit the transaction and both backends save the data. If there's a problem with one of the backends, the transaction is aborted in both regardless of the state of the other. It's also possible to abort the transaction manually, of course, causing a rollback on both backends as well. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726644116.0 transaction-5.0/pyproject.toml0000644000076500000240000000110514672477624015521 0ustar00jensstaff# # Generated from: # https://github.com/zopefoundation/meta/tree/master/config/pure-python [build-system] requires = ["setuptools<74"] build-backend = "setuptools.build_meta" [tool.coverage.run] branch = true source = ["transaction"] [tool.coverage.report] fail_under = 99 precision = 2 ignore_errors = true show_missing = true exclude_lines = ["pragma: no cover", "pragma: nocover", "except ImportError:", "raise NotImplementedError", "if __name__ == '__main__':", "self.fail", "raise AssertionError", "raise unittest.Skip"] [tool.coverage.html] directory = "parts/htmlcov" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1726644280.544432 transaction-5.0/setup.cfg0000644000076500000240000000074314672500071014414 0ustar00jensstaff[flake8] doctests = 1 [check-manifest] ignore = .editorconfig .meta.toml docs/_build/html/_sources/* docs/_build/doctest/* docs/_build/html/_static/placeholder.txt [isort] force_single_line = True combine_as_imports = True sections = FUTURE,STDLIB,THIRDPARTY,ZOPE,FIRSTPARTY,LOCALFOLDER known_third_party = docutils, pkg_resources, pytz known_zope = known_first_party = default_section = ZOPE line_length = 79 lines_after_imports = 2 [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726644148.0 transaction-5.0/setup.py0000644000076500000240000000545514672477664014337 0ustar00jensstaff############################################################################## # # Copyright (c) 2007 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## import os from setuptools import find_packages from setuptools import setup version = '5.0' here = os.path.abspath(os.path.dirname(__file__)) def _read_file(filename): with open(os.path.join(here, filename)) as f: return f.read() README = _read_file('README.rst') + '\n\n' + _read_file('CHANGES.rst') setup(name='transaction', version=version, description='Transaction management for Python', long_description=README, long_description_content_type='text/x-rst', classifiers=[ "Development Status :: 6 - Mature", "License :: OSI Approved :: Zope Public License", "Programming Language :: Python", "Topic :: Database", "Topic :: Software Development :: Libraries :: Python Modules", "Operating System :: Microsoft :: Windows", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Framework :: ZODB", ], author="Zope Foundation and Contributors", author_email="zodb-dev@zope.dev", url="https://github.com/zopefoundation/transaction", project_urls={ 'Issue Tracker': ('https://github.com/zopefoundation/' 'transaction/issues'), 'Sources': 'https://github.com/zopefoundation/transaction', }, license="ZPL 2.1", platforms=["any"], packages=find_packages('src'), package_dir={'': 'src'}, include_package_data=True, zip_safe=False, python_requires='>=3.8', install_requires=[ 'zope.interface', ], extras_require={ 'docs': ['Sphinx', 'repoze.sphinx.autointerface'], 'testing': ['coverage'], }, ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1726644280.5313325 transaction-5.0/src/0000755000076500000240000000000014672500071013356 5ustar00jensstaff././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1726644280.5407932 transaction-5.0/src/transaction/0000755000076500000240000000000014672500071015703 5ustar00jensstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/src/transaction/__init__.py0000644000076500000240000000413714632330173020021 0ustar00jensstaff############################################################################ # # Copyright (c) 2001, 2002, 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. # ############################################################################ """``transaction`` module: Exported transaction functions. """ # isort: off #: Default implementation of `~ITransaction` from transaction._transaction import Transaction # noqa: F401 unused import #: Default implementation of `~ISavepoint` from transaction._transaction import Savepoint # noqa: F401 unused import #: A single-threaded `~ITransactionManager` from transaction._manager import TransactionManager # noqa: F401 unused import #: A thread-safe `~ITransactionManager` from transaction._manager import ThreadTransactionManager # NB: "with transaction:" does not work because they worked # really hard to break looking up special methods like __enter__ and __exit__ # via getattr and getattribute; see http://bugs.python.org/issue12022. # 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726644116.0 transaction-5.0/src/transaction/_manager.py0000644000076500000240000002316314672477624020054 0ustar00jensstaff############################################################################ # # 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. # ############################################################################ """A TransactionManager controls transaction boundaries. It coordinates application code and resource managers, so that they are associated with the right transaction. """ import itertools import sys import threading from zope.interface import implementer from transaction._transaction import Transaction from transaction.interfaces import AlreadyInTransaction from transaction.interfaces import ITransactionManager from transaction.interfaces import NoTransaction from transaction.interfaces import TransientError from transaction.weakset import WeakSet # We have to remember sets of synch objects, especially Connections. # But we don't want mere registration with a transaction manager to # keep a synch object alive forever; in particular, it's common # practice not to explicitly close Connection objects, and keeping # a Connection alive keeps a potentially huge number of other objects # alive (e.g., the cache, and everything reachable from it too). # Therefore we use "weak sets" internally. # Call the ISynchronizer newTransaction() method on every element of # WeakSet synchs. # A transaction manager needs to do this whenever begin() is called. # Since it would be good if tm.get() returned the new transaction while # newTransaction() is running, calling this has to be delayed until after # the transaction manager has done whatever it needs to do to make its # get() return the new txn. def _new_transaction(txn, synchs): if synchs: synchs.map(lambda s: s.newTransaction(txn)) # Important: we must always pass a WeakSet (even if empty) to the Transaction # constructor: synchronizers are registered with the TM, but the # ISynchronizer xyzCompletion() methods are called by Transactions without # consulting the TM, so we need to pass a mutable collection of synchronizers # so that Transactions "see" synchronizers that get registered after the # Transaction object is constructed. @implementer(ITransactionManager) class TransactionManager: """Single-thread implementation of `~transaction.interfaces.ITransactionManager`. """ def __init__(self, explicit=False): self.explicit = explicit self._txn = None self._synchs = WeakSet() def begin(self): """See `~transaction.interfaces.ITransactionManager`.""" if self._txn is not None: if self.explicit: raise AlreadyInTransaction() self._txn.abort() txn = self._txn = Transaction(self._synchs, self) _new_transaction(txn, self._synchs) return txn def __enter__(self): return self.begin() def get(self): """See `~transaction.interfaces.ITransactionManager`.""" if self._txn is None: if self.explicit: raise NoTransaction() self._txn = Transaction(self._synchs, self) return self._txn def free(self, txn): if txn is not self._txn: raise ValueError("Foreign transaction") self._txn = None def registerSynch(self, synch): """ See `~transaction.interfaces.ITransactionManager`. """ self._synchs.add(synch) if self._txn is not None: synch.newTransaction(self._txn) def unregisterSynch(self, synch): """ See `~transaction.interfaces.ITransactionManager`. """ self._synchs.remove(synch) def clearSynchs(self): """ See `~transaction.interfaces.ITransactionManager`. """ self._synchs.clear() def registeredSynchs(self): """ See `~transaction.interfaces.ITransactionManager`. """ return bool(self._synchs) def isDoomed(self): """ See `~transaction.interfaces.ITransactionManager`. """ return self.get().isDoomed() def doom(self): """ See `~transaction.interfaces.ITransactionManager`. """ return self.get().doom() def commit(self): """ See `~transaction.interfaces.ITransactionManager`. """ return self.get().commit() def abort(self): """ See `~transaction.interfaces.ITransactionManager`. """ return self.get().abort() def __exit__(self, t, v, tb): if v is None: self.commit() else: self.abort() def savepoint(self, optimistic=False): """ See `~transaction.interfaces.ITransactionManager`. """ return self.get().savepoint(optimistic) def attempts(self, number=3): if number <= 0: raise ValueError("number must be positive") while number: number -= 1 if number: attempt = Attempt(self) yield attempt if attempt.success: break else: yield self def _retryable(self, error_type, error): if issubclass(error_type, TransientError): return True for dm in self.get()._resources: should_retry = getattr(dm, 'should_retry', None) if (should_retry is not None) and should_retry(error): return True return False run_no_func_types = int, type(None) def run(self, func=None, tries=3): if isinstance(func, self.run_no_func_types): if func is not None: tries = func return lambda func: self.run(func, tries) if tries <= 0: raise ValueError("tries must be > 0") # These are ordinarily strings, but that's # not required. A callable class could override them # to anything. name = func.__name__ or '' doc = func.__doc__ or '' if isinstance(name, bytes): name = name.decode('UTF-8') if isinstance(doc, bytes): doc = doc.decode('UTF-8') if name and name != '_': if doc: doc = name + '\n\n' + doc else: doc = name for try_no in itertools.count(1): txn = self.begin() if doc: txn.note(doc) try: result = func() 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 `. 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 graceful shutdown of data managers. """ def __init__(self): self.manager = TransactionManager() @property def explicit(self): return self.manager.explicit @explicit.setter def explicit(self, v): self.manager.explicit = v def begin(self): return self.manager.begin() def get(self): return self.manager.get() def __enter__(self): return self.manager.__enter__() def commit(self): return self.manager.commit() def abort(self): return self.manager.abort() def __exit__(self, t, v, tb): return self.manager.__exit__(t, v, tb) def doom(self): return self.manager.doom() def isDoomed(self): return self.manager.isDoomed() def savepoint(self, optimistic=False): return self.manager.savepoint(optimistic) def registerSynch(self, synch): return self.manager.registerSynch(synch) def unregisterSynch(self, synch): return self.manager.unregisterSynch(synch) def clearSynchs(self): return self.manager.clearSynchs() def registeredSynchs(self): return self.manager.registeredSynchs() def attempts(self, number=3): return self.manager.attempts(number) def run(self, func=None, tries=3): return self.manager.run(func, tries) class Attempt: success = False def __init__(self, manager): self.manager = manager def _retry_or_raise(self, t, v, tb): retry = self.manager._retryable(t, v) self.manager.abort() if retry: return retry # suppress the exception if necessary raise v.with_traceback(tb) # otherwise reraise the exception def __enter__(self): return self.manager.__enter__() def __exit__(self, t, v, tb): if v is None: try: self.manager.commit() except: # noqa: E722 do not use bare 'except' return self._retry_or_raise(*sys.exc_info()) else: self.success = True else: return self._retry_or_raise(t, v, tb) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726644116.0 transaction-5.0/src/transaction/_transaction.py0000644000076500000240000005555514672477624021001 0ustar00jensstaff############################################################################ # # 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. # ############################################################################ import logging import sys import threading import traceback import warnings import weakref from io import StringIO from zope.interface import implementer from transaction import interfaces from transaction.interfaces import TransactionFailedError from transaction.weakset import WeakSet _marker = object() _TB_BUFFER = None # unittests may hook def _makeTracebackBuffer(): # pragma NO COVER if _TB_BUFFER is not None: return _TB_BUFFER return StringIO() _LOGGER = None # unittests may hook def _makeLogger(): # pragma NO COVER if _LOGGER is not None: return _LOGGER return logging.getLogger("txn.%d" % threading.get_ident()) class Status: # ACTIVE is the initial state. ACTIVE = "Active" COMMITTING = "Committing" COMMITTED = "Committed" DOOMED = "Doomed" # commit() or commit(True) raised an exception. All further attempts # to commit or join this transaction will raise TransactionFailedError. COMMITFAILED = "Commit failed" class _NoSynchronizers: @staticmethod def map(_f): """Do nothing.""" @implementer(interfaces.ITransaction) class Transaction: """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 # time. _savepoint_index = 0 # If savepoints are used, keep a weak key dict of them. This maps a # savepoint to its index (see above). _savepoint2index = None # Meta data. extended_info is also metadata, but is initialized to an # empty dict in __init__. _user = "" _description = "" def __init__(self, synchronizers=None, manager=None): self.status = Status.ACTIVE # List of resource managers, e.g. MultiObjectResourceAdapters. self._resources = [] # Weak set of synchronizer objects to call. if synchronizers is None: synchronizers = WeakSet() self._synchronizers = synchronizers self._manager = manager # _adapters: Connection/_p_jar -> MultiObjectResourceAdapter[Sub] self._adapters = {} self._voted = {} # id(Connection) -> boolean, True if voted # _voted and other dictionaries use the id() of the resource # manager as a key, because we can't guess whether the actual # resource managers will be safe to use as dict keys. # The user, description, and extension attributes are accessed # directly by storages, leading underscore notwithstanding. self.extension = {} self.log = _makeLogger() self.log.debug("new transaction") # If a commit fails, the traceback is saved in _failure_traceback. # If another attempt is made to commit, TransactionFailedError is # raised, incorporating this traceback. self._failure_traceback = None # List of (hook, args, kws) tuples added by addBeforeCommitHook(). self._before_commit = [] # 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 # absent any formal API. return self.extension @_extension.setter def _extension(self, v): self.extension = v @property def user(self): return self._user @user.setter def user(self, v): if v is None: raise ValueError("user must not be None") self._user = text_or_warn(v) @property def description(self): return self._description @description.setter def description(self, v): if v is not None: self._description = text_or_warn(v) def isDoomed(self): """See `~transaction.interfaces.ITransaction`.""" return self.status is Status.DOOMED def doom(self): """See `~transaction.interfaces.ITransaction`.""" if self.status is not Status.DOOMED: if self.status is not Status.ACTIVE: # should not doom transactions in the middle, # or after, a commit raise ValueError('non-doomable') self.status = Status.DOOMED # Raise TransactionFailedError, due to commit()/join()/register() # getting called when the current transaction has already suffered # a commit/savepoint failure. def _prior_operation_failed(self): assert self._failure_traceback is not None raise TransactionFailedError( "An operation previously failed, with traceback:\n\n%s" % self._failure_traceback.getvalue()) def join(self, resource): """See `~transaction.interfaces.ITransaction`.""" if self.status is Status.COMMITFAILED: self._prior_operation_failed() # doesn't return if (self.status is not Status.ACTIVE and self.status is not Status.DOOMED): # TODO: Should it be possible to join a committing transaction? # I think some users want it. raise ValueError( f"expected txn status {Status.ACTIVE!r} or {Status.DOOMED!r}," f" but it's {self.status!r}") self._resources.append(resource) if self._savepoint2index: # A data manager has joined a transaction *after* a savepoint # was created. A couple of things are different in this case: # # 1. We need to add its savepoint to all previous savepoints. # so that if they are rolled back, we roll this one back too. # # 2. We don't actually need to ask the data manager for a # savepoint: because it's just joining, we can just abort it to # roll back to the current state, so we simply use an # AbortSavepoint. datamanager_savepoint = AbortSavepoint(resource, self) for transaction_savepoint in self._savepoint2index.keys(): transaction_savepoint._savepoints.append( datamanager_savepoint) def _unjoin(self, resource): # Leave a transaction because a savepoint was rolled back on a resource # that joined later. # Don't use remove. We don't want to assume anything about __eq__. self._resources = [r for r in self._resources if r is not resource] def savepoint(self, optimistic=False): """See `~transaction.interfaces.ITransaction`.""" if self.status is Status.COMMITFAILED: self._prior_operation_failed() # doesn't return, it raises try: savepoint = Savepoint(self, optimistic, *self._resources) except: # noqa: E722 do not use bare 'except' self._cleanup(self._resources) self._saveAndRaiseCommitishError() # reraises! if self._savepoint2index is None: self._savepoint2index = weakref.WeakKeyDictionary() self._savepoint_index += 1 self._savepoint2index[savepoint] = self._savepoint_index return savepoint # Remove and invalidate all savepoints we know about with an index # larger than `savepoint`'s. This is what's needed when a rollback # _to_ `savepoint` is done. def _remove_and_invalidate_after(self, savepoint): savepoint2index = self._savepoint2index index = savepoint2index[savepoint] # use list(items()) to make copy to avoid mutating while iterating for savepoint, i in list(savepoint2index.items()): if i > index: savepoint.transaction = None # invalidate del savepoint2index[savepoint] # Invalidate and forget about all savepoints. def _invalidate_all_savepoints(self): for savepoint in self._savepoint2index.keys(): savepoint.transaction = None # invalidate self._savepoint2index.clear() def commit(self): """See `~transaction.interfaces.ITransaction`.""" if self.status is Status.DOOMED: raise interfaces.DoomedTransaction( 'transaction doomed, cannot commit') if self._savepoint2index: self._invalidate_all_savepoints() if self.status is Status.COMMITFAILED: self._prior_operation_failed() # doesn't return self._callBeforeCommitHooks() self._synchronizers.map(lambda s: s.beforeCompletion(self)) self.status = Status.COMMITTING try: self._commitResources() self.status = Status.COMMITTED except: # noqa: E722 do not use bare 'except' t = None v = None tb = None try: t, v, tb = self._saveAndGetCommitishError() self._callAfterCommitHooks(status=False) raise v.with_traceback(tb) finally: del t, v, tb else: self._synchronizers.map(lambda s: s.afterCompletion(self)) self._callAfterCommitHooks(status=True) self._free() self.log.debug("commit") def _saveAndGetCommitishError(self): self.status = Status.COMMITFAILED # Save the traceback for TransactionFailedError. ft = self._failure_traceback = _makeTracebackBuffer() t = None v = None tb = None try: t, v, tb = sys.exc_info() # Record how we got into commit(). traceback.print_stack(sys._getframe(1), None, ft) # Append the stack entries from here down to the exception. traceback.print_tb(tb, None, ft) # Append the exception type and value. ft.writelines(traceback.format_exception_only(t, v)) return t, v, tb finally: del t, v, tb def _saveAndRaiseCommitishError(self): t = None v = None tb = None try: t, v, tb = self._saveAndGetCommitishError() raise v.with_traceback(tb) finally: del t, v, tb def getBeforeCommitHooks(self): """See `~transaction.interfaces.ITransaction`.""" return iter(self._before_commit) def addBeforeCommitHook(self, hook, args=(), kws=None): """See `~transaction.interfaces.ITransaction`.""" if kws is None: kws = {} self._before_commit.append((hook, tuple(args), kws)) def _callBeforeCommitHooks(self): # Call all hooks registered, allowing further registrations # during processing. self._call_hooks(self._before_commit) def getAfterCommitHooks(self): """See `~transaction.interfaces.ITransaction`.""" return iter(self._after_commit) def addAfterCommitHook(self, hook, args=(), kws=None): """See `~transaction.interfaces.ITransaction`.""" if kws is None: kws = {} self._after_commit.append((hook, tuple(args), kws)) def _callAfterCommitHooks(self, status=True): self._call_hooks(self._after_commit, exc=False, clean=True, prefix_args=(status,)) def _call_hooks(self, hooks, exc=True, clean=False, prefix_args=()): """Call *hooks*. If *exc* is true, fail on the first exception; otherwise log the exception and continue. If *clean* is true, abort all resources. This is to ensure a clean state should a (after) hook has affected one of the resources. *prefix_args* defines 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: # noqa: E722 do not use bare '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 clean: # 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: # noqa: E722 do not use bare '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. 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. L = list(self._resources) L.sort(key=rm_key) try: for rm in L: rm.tpc_begin(self) for rm in L: rm.commit(self) self.log.debug("commit %r", rm) for rm in L: rm.tpc_vote(self) self._voted[id(rm)] = True try: for rm in L: rm.tpc_finish(self) except: # noqa: E722 do not use bare 'except' # TODO: do we need to make this warning stronger? # TODO: It would be nice if the system could be configured # to stop committing transactions at this point. self.log.critical("A storage error occurred during the second " "phase of the two-phase commit. Resources " "may be in an inconsistent state.") raise except: # noqa: E722 do not use bare 'except' # If an error occurs committing a transaction, we try # to revert the changes in each of the resource managers. t, v, tb = sys.exc_info() try: try: self._cleanup(L) finally: self._synchronizers.map(lambda s: s.afterCompletion(self)) raise v.with_traceback(tb) finally: del t, v, tb def _cleanup(self, L): # Called when an exception occurs during tpc_vote or tpc_finish. for rm in L: if id(rm) not in self._voted: try: rm.abort(self) except Exception: self.log.error("Error in abort() on manager %s", rm, exc_info=sys.exc_info()) for rm in L: try: rm.tpc_abort(self) 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 # 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 except AttributeError: raise KeyError(ob) try: return data[id(ob)] except KeyError: raise KeyError(ob) def set_data(self, ob, ob_data): try: data = self._data except AttributeError: data = self._data = {} data[id(ob)] = ob_data def abort(self): """See `~transaction.interfaces.ITransaction`.""" 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: # noqa: E722 do not use bare '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) except: # noqa: E722 do not use bare 'except' if tb is None: t, v, tb = sys.exc_info() self.log.error("Failed to abort resource manager: %s", rm, exc_info=sys.exc_info()) 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)) self.log.debug("abort") if tb is not None: raise v.with_traceback(tb) finally: self._free() del t, v, tb def note(self, text): """See `~transaction.interfaces.ITransaction`.""" if text is not None: text = text_or_warn(text).strip() if self.description: self.description += "\n" + text else: self.description = text def setUser(self, user_name, path="/"): """See `~transaction.interfaces.ITransaction`.""" self.user = f"{text_or_warn(path)} {text_or_warn(user_name)}" def setExtendedInfo(self, name, value): """See `~transaction.interfaces.ITransaction`.""" self.extension[name] = value def isRetryableError(self, error): return self._manager._retryable(type(error), error) # TODO: We need a better name for the adapters. def rm_key(rm): func = getattr(rm, 'sortKey', None) if func is not None: return func() @implementer(interfaces.ISavepoint) class Savepoint: """Implementation of `~transaction.interfaces.ISavepoint`, a transaction savepoint. Transaction savepoints coordinate savepoints for data managers participating in a transaction. """ def __init__(self, transaction, optimistic, *resources): self.transaction = transaction self._savepoints = savepoints = [] for datamanager in resources: try: savepoint = datamanager.savepoint except AttributeError: if not optimistic: raise TypeError("Savepoints unsupported", datamanager) savepoint = NoRollbackSavepoint(datamanager) else: savepoint = savepoint() savepoints.append(savepoint) @property def valid(self): return self.transaction is not None def rollback(self): """See `~transaction.interfaces.ISavepoint`.""" transaction = self.transaction if transaction is None: raise interfaces.InvalidSavepointRollbackError( 'invalidated by a later savepoint') transaction._remove_and_invalidate_after(self) try: for savepoint in self._savepoints: savepoint.rollback() except: # noqa: E722 do not use bare 'except' # Mark the transaction as failed. transaction._saveAndRaiseCommitishError() # reraises! class AbortSavepoint: def __init__(self, datamanager, transaction): self.datamanager = datamanager self.transaction = transaction def rollback(self): self.datamanager.abort(self.transaction) self.transaction._unjoin(self.datamanager) class NoRollbackSavepoint: def __init__(self, datamanager): self.datamanager = datamanager def rollback(self): raise TypeError("Savepoints unsupported", self.datamanager) def text_or_warn(s): if isinstance(s, str): return s warnings.warn("Expected text", DeprecationWarning, stacklevel=3) if isinstance(s, bytes): return s.decode('utf-8', 'replace') else: return str(s) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/src/transaction/interfaces.py0000644000076500000240000005750014632330173020407 0ustar00jensstaff############################################################################## # # Copyright (c) 2001, 2002 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. # ############################################################################## from zope.interface import Attribute from zope.interface import Interface class ITransactionManager(Interface): """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( """Explicit mode indicator. 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`. .. versionadded:: 2.1.0 """) def begin(): """Explicitly begin and return a new transaction. If an existing transaction is in progress and the transaction 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.. 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, ``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.) """ def get(): """Get the current transaction. In explicit mode, if a transaction hasn't begun, a `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. """ def abort(): """Abort the current transaction. In explicit mode, if a transaction hasn't begun, a `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. """ def isDoomed(): """Return True if the current transaction is doomed, otherwise False. In explicit mode, if a transaction hasn't begun, a `NoTransaction` exception will be raised. """ def savepoint(optimistic=False): """Create a savepoint from the current transaction. 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. In explicit mode, if a transaction hasn't begun, a `NoTransaction` exception will be raised. """ def registerSynch(synch): """Register an `ISynchronizer`. Synchronizers are notified about some major events in a transaction's life. See `ISynchronizer` for details. If a synchronizer registers while there is an active transaction, its ``newTransaction`` method will be called with the active transaction. """ def unregisterSynch(synch): """Unregister an `ISynchronizer`. Synchronizers are notified about some major events in a transaction's life. See `ISynchronizer` for details. """ def clearSynchs(): """Unregister all registered `ISynchronizer` objects. This exists to support test cleanup/initialization """ def registeredSynchs(): """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.""" 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, when a transaction commits. A storage may impose a limit on the size of the value; behavior is undefined if such a limit is exceeded (for example, a storage may raise an exception, or truncate the value). """) 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, when a transaction commits. A storage may impose a limit on the size of the description; behavior is undefined if such a limit is exceeded (for example, a storage may raise an exception, or truncate the value). """) extension = Attribute( "A dictionary containing application-defined metadata.") def commit(): """Finalize the transaction. This executes the two-phase commit algorithm for all `IDataManager` objects associated with the transaction. """ def abort(): """Abort the transaction. This is called from the application. This can only be called before the two-phase commit protocol has been started. """ def doom(): """Doom the transaction. Dooms the current transaction. This will cause `DoomedTransaction` to be raised on any attempt to commit the transaction. Otherwise the transaction will behave as if it was active. """ def savepoint(optimistic=False): """Create a savepoint. 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. """ def join(datamanager): """Add a data manager to the transaction. *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 becomes its value, else two newlines and the stripped text are appended to `description`. """ def setExtendedInfo(name, value): """Add extension data to the transaction. :param text name: is the text (unicode) name of the extension property to set :param value: must be picklable and json serializable Multiple calls may be made to set multiple extension properties, provided the names are distinct. Storages record the extension data, as meta-data, when a transaction commits. A storage may impose a limit on the size of extension data; behavior is undefined if such a limit is exceeded (for example, a storage may raise an exception, or remove `` pairs). """ def addBeforeCommitHook(hook, args=(), kws=None): """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. :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 registered `addBeforeCommitHook` 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. """ def addAfterCommitHook(hook, args=(), kws=None): """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* 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 `addAfterCommitHook` 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 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 For objects such as data managers or their subobjects that work with multiple transactions, it's convenient to store transaction-specific data on the transaction itself. The transaction knows nothing about the data, but simply holds it on behalf of the object. The object passed should be the object that needs the data, as 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`. """ def isRetryableError(error): """Determine if the error is retryable. 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 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.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 manager's constructor. """) def abort(transaction): """Abort a transaction and forget all changes. Abort must be called outside of a two-phase commit. Abort is called by the transaction manager to abort transactions that are not yet in a two-phase commit. It may also be called when rolling back a savepoint made before the data manager joined the transaction. In any case, after abort is called, the data manager is no longer participating in the transaction. If there are new changes, the data manager must rejoin the transaction. """ def tpc_begin(transaction): """Begin commit of a transaction, starting the two-phase commit. *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. """ def tpc_vote(transaction): """Verify that a data manager can commit the transaction. This is the last chance for a data manager to vote 'no'. A data manager votes 'no' by raising an exception. *transaction* is the `ITransaction` instance associated with the transaction being committed. """ def tpc_finish(transaction): """Indicate confirmation that the transaction is done. Make all changes to objects modified by this transaction persist. *transaction* is the `ITransaction` instance associated with the transaction being committed. This should never fail. If this raises an exception, the database is not expected to maintain consistency; it's a serious error. """ def tpc_abort(transaction): """Abort a transaction. This is called by a transaction manager to end a two-phase commit on the data manager. Abandon all changes to objects modified by this transaction. *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 `IDataManagers`. In order to guarantee a total ordering, keys **must** be `strings `. 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. # # This allows ordering multiple connections that use the same storage # in a consistent manner. This is unique for the lifetime of a # connection, which is good enough to avoid ZEO deadlocks. # """ class ISavepointDataManager(IDataManager): def savepoint(): """Return a data-manager savepoint (`IDataManagerSavepoint`).""" class IRetryDataManager(IDataManager): def should_retry(exception): """Return whether a given exception instance should be retried. 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 considering whether to retry a failed transaction. """ class IDataManagerSavepoint(Interface): """Savepoint for data-manager changes for use in 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. """ def rollback(): """Rollback any work done since the savepoint. """ class ISavepoint(Interface): """A transaction savepoint. """ def rollback(): """Rollback any work done since the savepoint. `InvalidSavepointRollbackError` is raised if the savepoint isn't valid. """ valid = Attribute( "Boolean indicating whether the savepoint is valid") class InvalidSavepointRollbackError(Exception): """Attempt to rollback an invalid savepoint. A savepoint may be invalid because: - The surrounding transaction has committed or aborted. - An earlier savepoint in the same transaction has been rolled back. """ class ISynchronizer(Interface): """Objects that participate in the transaction-boundary notification API. """ def beforeCompletion(transaction): """Hook that is called by the transaction at the start of a commit.""" def afterCompletion(transaction): """Hook that is called by the transaction after completing a commit.""" def newTransaction(transaction): """Hook that is called at the start of a transaction. This hook is called when, and only when, a transaction manager's `~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. 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): """A commit was attempted on a transaction that was doomed.""" class TransientError(TransactionError): """An error has occured when performing a transaction. It's possible that retrying the transaction will succeed. """ class NoTransaction(TransactionError): """No transaction has been defined An application called an operation on a transaction manager that 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 `~ITransactionManager.begin` on a transaction manager in explicit mode, without committing or aborting the previous transaction. .. versionadded:: 2.1.0 """ ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1726644280.543239 transaction-5.0/src/transaction/tests/0000755000076500000240000000000014672500071017045 5ustar00jensstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/src/transaction/tests/__init__.py0000644000076500000240000000000214632330173021146 0ustar00jensstaff# ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/src/transaction/tests/common.py0000644000076500000240000000405614632330173020714 0ustar00jensstaff############################################################################## # # Copyright (c) 2012 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE # ############################################################################## class DummyFile: def __init__(self): self._lines = [] def write(self, text): self._lines.append(text) def writelines(self, lines): self._lines.extend(lines) class DummyLogger: def __init__(self): self._clear() def _clear(self): self._log = [] def log(self, level, msg, *args, **kwargs): if args: self._log.append((level, msg % args)) else: self._log.append((level, msg)) def debug(self, msg, *args, **kw): self.log('debug', msg, *args, **kw) def error(self, msg, *args, **kw): self.log('error', msg, *args, **kw) def critical(self, msg, *args, **kw): self.log('critical', msg, *args, **kw) class Monkey: # context-manager for replacing module names in the scope of a test. def __init__(self, module, **kw): self.module = module self.to_restore = {key: getattr(module, key) for key in kw} for key, value in kw.items(): setattr(module, key, value) def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): for key, value in self.to_restore.items(): setattr(self.module, key, value) def assertRaisesEx(e_type, checked, *args, **kw): # Only used in doctests try: checked(*args, **kw) except e_type as e: return e raise AssertionError("Didn't raise: %s" % e_type.__name__) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726644116.0 transaction-5.0/src/transaction/tests/examples.py0000644000076500000240000000723414672477624021264 0ustar00jensstaff############################################################################## # # Copyright (c) 2004 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Sample objects for use in tests """ class DataManager: """ Sample data manager. Used by the 'datamanager' chapter in the Sphinx docs. """ def __init__(self): self.state = 0 self.sp = 0 self.transaction = None self.delta = 0 self.txn_state = None self.begun = False def _check_state(self, *ok_states): if self.txn_state not in ok_states: raise ValueError("txn in state %r but expected one of %r" % (self.txn_state, ok_states)) def _checkTransaction(self, transaction): if (transaction is not self.transaction and self.transaction is not None): raise TypeError("Transaction missmatch", transaction, self.transaction) def inc(self, n=1): self.delta += n def tpc_begin(self, transaction): self._checkTransaction(transaction) self._check_state(None) self.transaction = transaction self.txn_state = 'tpc_begin' self.begun = True def tpc_vote(self, transaction): self._checkTransaction(transaction) self._check_state('tpc_begin') self.state += self.delta self.txn_state = 'tpc_vote' def tpc_finish(self, transaction): self._checkTransaction(transaction) self._check_state('tpc_vote') self.delta = 0 self.transaction = None self.txn_state = None def tpc_abort(self, transaction): self._checkTransaction(transaction) if self.transaction is not None: self.transaction = None if self.txn_state == 'tpc_vote': self.state -= self.delta self.txn_state = None self.delta = 0 def savepoint(self, transaction): if self.txn_state is not None: raise AssertionError("Can't get savepoint during two-phase commit") self._checkTransaction(transaction) self.transaction = transaction self.sp += 1 return SavePoint(self) 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: def __init__(self, rm): self.rm = rm self.sp = rm.sp self.delta = rm.delta self.transaction = rm.transaction def rollback(self): if self.transaction is not self.rm.transaction: raise TypeError("Attempt to rollback stale rollback") if self.rm.sp < self.sp: raise TypeError("Attempt to roll back to invalid save point", self.sp, self.rm.sp) self.rm.sp = self.sp self.rm.delta = self.delta def discard(self): "Does nothing." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/src/transaction/tests/savepointsample.py0000644000076500000240000001564414632330173022643 0ustar00jensstaff############################################################################## # # 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. # ############################################################################## """Savepoint data manager implementation example. Sample data manager implementation that illustrates how to implement savepoints. Used by savepoint.rst in the Sphinx docs. """ from zope.interface import implementer import transaction.interfaces @implementer(transaction.interfaces.IDataManager) class SampleDataManager: """Sample implementation of data manager that doesn't support savepoints This data manager stores named simple values, like strings and numbers. """ def __init__(self, transaction_manager=None): if transaction_manager is None: # Use the thread-local transaction manager if none is provided: import transaction transaction_manager = transaction.manager self.transaction_manager = transaction_manager # Our committed and uncommitted data: self.committed = {} self.uncommitted = self.committed.copy() # Our transaction state: # # If our uncommitted data is modified, we'll join a transaction # and keep track of the transaction we joined. Any commit # related messages we get should be for this same transaction self.transaction = None # What phase, if any, of two-phase commit we are in: self.tpc_phase = None # ###################################################################### # Provide a mapping interface to uncommitted data. We provide # a basic subset of the interface. DictMixin does the rest. def __getitem__(self, name): return self.uncommitted[name] def __setitem__(self, name, value): self._join() # join the current transaction, if we haven't already self.uncommitted[name] = value def keys(self): return self.uncommitted.keys() __iter__ = keys def __contains__(self, k): return k in self.uncommitted def __repr__(self): return repr(self.uncommitted) # ####################################################################### ####################################################################### # Transaction methods def _join(self): # If this is the first change in the transaction, join the transaction if self.transaction is None: self.transaction = self.transaction_manager.get() self.transaction.join(self) def _resetTransaction(self): self.last_note = getattr(self.transaction, 'description', None) self.transaction = None self.tpc_phase = None def abort(self, transaction): """Throw away changes made before the commit process has started.""" assert ((transaction is self.transaction) or (self.transaction is None) ), "Must not change transactions" assert self.tpc_phase is None, "Must be called outside of tpc" self.uncommitted = self.committed.copy() self._resetTransaction() def tpc_begin(self, transaction): """Enter two-phase commit.""" assert transaction is self.transaction, "Must not change transactions" assert self.tpc_phase is None, "Must be called outside of tpc" self.tpc_phase = 1 def commit(self, transaction): """Record data modified during the transaction.""" assert transaction is self.transaction, "Must not change transactions" assert self.tpc_phase == 1, "Must be called in first phase of tpc" # In our simple example, we don't need to do anything. # A more complex data manager would typically write to some sort # of log. def tpc_vote(self, transaction): assert transaction is self.transaction, "Must not change transactions" assert self.tpc_phase == 1, "Must be called in first phase of tpc" # This particular data manager is always ready to vote. # Real data managers will usually need to take some steps to # make sure that the finish will succeed self.tpc_phase = 2 def tpc_finish(self, transaction): assert transaction is self.transaction, "Must not change transactions" assert self.tpc_phase == 2, "Must be called in second phase of tpc" self.committed = self.uncommitted.copy() self._resetTransaction() def tpc_abort(self, transaction): if self.transaction is not None: # pragma: no cover # otherwise we're not actually joined. assert self.tpc_phase is not None, "Must be called inside of tpc" self.uncommitted = self.committed.copy() self._resetTransaction() # ####################################################################### ####################################################################### # Other data manager methods def sortKey(self): # Commit operations on multiple data managers are performed in # sort key order. This important to avoid deadlock when data # managers are shared among multiple threads or processes and # use locks to manage that sharing. We aren't going to bother # with that here. return str(id(self)) # ####################################################################### @implementer(transaction.interfaces.ISavepointDataManager) class SampleSavepointDataManager(SampleDataManager): """Sample implementation of a savepoint-supporting data manager This extends the basic data manager with savepoint support. """ def savepoint(self): # When we create the savepoint, we save the existing database state. return SampleSavepoint(self, self.uncommitted.copy()) def _rollback_savepoint(self, savepoint): # When we rollback the savepoint, we restore the saved data. # Caution: without the copy(), further changes to the database # could reflect in savepoint.data, and then `savepoint` would no # longer contain the originally saved data, and so `savepoint` # couldn't restore the original state if a rollback to this # savepoint was done again. IOW, copy() is necessary. self.uncommitted = savepoint.data.copy() @implementer(transaction.interfaces.IDataManagerSavepoint) class SampleSavepoint: def __init__(self, data_manager, data): self.data_manager = data_manager self.data = data def rollback(self): self.data_manager._rollback_savepoint(self) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726644116.0 transaction-5.0/src/transaction/tests/test__manager.py0000644000076500000240000007542014672477624022260 0ustar00jensstaff############################################################################## # # Copyright (c) 2012 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE # ############################################################################## import unittest from unittest import mock import zope.interface.verify from .. import interfaces class TransactionManagerTests(unittest.TestCase): def _getTargetClass(self): from transaction import TransactionManager return TransactionManager def _makeOne(self): return self._getTargetClass()() def _makePopulated(self): mgr = self._makeOne() sub1 = DataObject(mgr) sub2 = DataObject(mgr) sub3 = DataObject(mgr) nosub1 = DataObject(mgr, nost=1) return mgr, sub1, sub2, sub3, nosub1 def test_interface(self): zope.interface.verify.verifyObject(interfaces.ITransactionManager, self._makeOne()) def test_ctor(self): tm = self._makeOne() self.assertIsNone(tm._txn) self.assertEqual(len(tm._synchs), 0) def test_begin_wo_existing_txn_wo_synchs(self): from transaction._transaction import Transaction tm = self._makeOne() tm.begin() self.assertIsInstance(tm._txn, Transaction) def test_begin_wo_existing_txn_w_synchs(self): from transaction._transaction import Transaction tm = self._makeOne() synch = DummySynch() tm.registerSynch(synch) tm.begin() self.assertIsInstance(tm._txn, Transaction) self.assertIn(tm._txn, synch._txns) def test_begin_w_existing_txn(self): class Existing: _aborted = False def abort(self): self._aborted = True tm = self._makeOne() tm._txn = txn = Existing() tm.begin() self.assertIsNot(tm._txn, txn) self.assertTrue(txn._aborted) def test_get_wo_existing_txn(self): from transaction._transaction import Transaction tm = self._makeOne() txn = tm.get() self.assertIsInstance(txn, Transaction) def test_get_w_existing_txn(self): class Existing: _aborted = False def abort(self): raise AssertionError("This is not actually called") tm = self._makeOne() tm._txn = txn = Existing() self.assertIs(tm.get(), txn) def test_free_w_other_txn(self): from transaction._transaction import Transaction tm = self._makeOne() txn = Transaction() tm.begin() self.assertRaises(ValueError, tm.free, txn) def test_free_w_existing_txn(self): class Existing: _aborted = False def abort(self): raise AssertionError("This is not actually called") tm = self._makeOne() tm._txn = txn = Existing() tm.free(txn) self.assertIsNone(tm._txn) def test_registerSynch(self): tm = self._makeOne() synch = DummySynch() tm.registerSynch(synch) self.assertEqual(len(tm._synchs), 1) self.assertIn(synch, tm._synchs) def test_unregisterSynch(self): tm = self._makeOne() synch1 = DummySynch() synch2 = DummySynch() self.assertFalse(tm.registeredSynchs()) tm.registerSynch(synch1) self.assertTrue(tm.registeredSynchs()) tm.registerSynch(synch2) self.assertTrue(tm.registeredSynchs()) tm.unregisterSynch(synch1) self.assertTrue(tm.registeredSynchs()) self.assertEqual(len(tm._synchs), 1) self.assertNotIn(synch1, tm._synchs) self.assertIn(synch2, tm._synchs) tm.unregisterSynch(synch2) self.assertFalse(tm.registeredSynchs()) def test_clearSynchs(self): tm = self._makeOne() synch1 = DummySynch() synch2 = DummySynch() tm.registerSynch(synch1) tm.registerSynch(synch2) tm.clearSynchs() self.assertEqual(len(tm._synchs), 0) def test_isDoomed_wo_existing_txn(self): tm = self._makeOne() self.assertFalse(tm.isDoomed()) tm._txn.doom() self.assertTrue(tm.isDoomed()) def test_isDoomed_w_existing_txn(self): class Existing: _doomed = False def isDoomed(self): return self._doomed tm = self._makeOne() tm._txn = txn = Existing() self.assertFalse(tm.isDoomed()) txn._doomed = True self.assertTrue(tm.isDoomed()) def test_doom(self): tm = self._makeOne() txn = tm.get() self.assertFalse(txn.isDoomed()) tm.doom() self.assertTrue(txn.isDoomed()) self.assertTrue(tm.isDoomed()) def test_commit_w_existing_txn(self): class Existing: _committed = False def commit(self): self._committed = True tm = self._makeOne() tm._txn = txn = Existing() tm.commit() self.assertTrue(txn._committed) def test_abort_w_existing_txn(self): class Existing: _aborted = False def abort(self): self._aborted = True tm = self._makeOne() tm._txn = txn = Existing() tm.abort() self.assertTrue(txn._aborted) def test_as_context_manager_wo_error(self): class _Test: _committed = False _aborted = False def commit(self): self._committed = True def abort(self): raise AssertionError("This should not be called") tm = self._makeOne() with tm: tm._txn = txn = _Test() self.assertTrue(txn._committed) self.assertFalse(txn._aborted) def test_as_context_manager_w_error(self): class _Test: _committed = False _aborted = False def commit(self): raise AssertionError("This should not be called") def abort(self): self._aborted = True tm = self._makeOne() with self.assertRaises(ZeroDivisionError): with tm: tm._txn = txn = _Test() raise ZeroDivisionError() self.assertFalse(txn._committed) self.assertTrue(txn._aborted) def test_savepoint_default(self): class _Test: _sp = None def savepoint(self, optimistic): self._sp = optimistic tm = self._makeOne() tm._txn = txn = _Test() tm.savepoint() self.assertFalse(txn._sp) def test_savepoint_explicit(self): class _Test: _sp = None def savepoint(self, optimistic): self._sp = optimistic tm = self._makeOne() tm._txn = txn = _Test() tm.savepoint(True) self.assertTrue(txn._sp) def test_attempts_w_invalid_count(self): tm = self._makeOne() self.assertRaises(ValueError, list, tm.attempts(0)) self.assertRaises(ValueError, list, tm.attempts(-1)) self.assertRaises(ValueError, list, tm.attempts(-10)) def test_attempts_w_valid_count(self): tm = self._makeOne() found = list(tm.attempts(1)) self.assertEqual(len(found), 1) self.assertIs(found[0], tm) def test_attempts_stop_on_success(self): tm = self._makeOne() i = 0 for attempt in tm.attempts(): with attempt: i += 1 self.assertEqual(i, 1) def test_attempts_retries(self): import transaction.interfaces class Retry(transaction.interfaces.TransientError): pass tm = self._makeOne() i = 0 for attempt in tm.attempts(4): with attempt: i += 1 if i < 4: raise Retry self.assertEqual(i, 4) def test_attempts_retries_but_gives_up(self): import transaction.interfaces class Retry(transaction.interfaces.TransientError): pass tm = self._makeOne() i = 0 with self.assertRaises(Retry): for attempt in tm.attempts(4): with attempt: i += 1 raise Retry self.assertEqual(i, 4) def test_attempts_propigates_errors(self): tm = self._makeOne() with self.assertRaises(ValueError): for attempt in tm.attempts(4): with attempt: raise ValueError def test_attempts_defer_to_dm(self): import transaction.tests.savepointsample class DM(transaction.tests.savepointsample.SampleSavepointDataManager): def should_retry(self, e): if 'should retry' in str(e): return True ntry = 0 dm = transaction.tests.savepointsample.SampleSavepointDataManager() dm2 = DM() with transaction.manager: dm2['ntry'] = 0 for attempt in transaction.manager.attempts(): with attempt: ntry += 1 dm['ntry'] = ntry dm2['ntry'] = ntry if ntry % 3: raise ValueError('we really should retry this') self.assertEqual(ntry, 3) def test_attempts_w_default_count(self): from transaction._manager import Attempt tm = self._makeOne() found = list(tm.attempts()) self.assertEqual(len(found), 3) for attempt in found[:-1]: self.assertIsInstance(attempt, Attempt) self.assertIs(attempt.manager, tm) self.assertIs(found[-1], tm) def test_run(self): import transaction.interfaces class Retry(transaction.interfaces.TransientError): pass tm = self._makeOne() i = [0, None] @tm.run() def meaning(): """Nice doc""" i[0] += 1 i[1] = tm.get() if i[0] < 3: raise Retry return 42 self.assertEqual(i[0], 3) self.assertEqual(meaning, 42) self.assertEqual(i[1].description, "meaning\n\nNice doc") def test_run_no_name_explicit_tries(self): import transaction.interfaces class Retry(transaction.interfaces.TransientError): pass tm = self._makeOne() i = [0, None] @tm.run(4) def _(): """Nice doc""" i[0] += 1 i[1] = tm.get() if i[0] < 4: raise Retry self.assertEqual(i[0], 4) self.assertEqual(i[1].description, "Nice doc") def test_run_pos_tries(self): tm = self._makeOne() with self.assertRaises(ValueError): tm.run(0)(lambda: None) with self.assertRaises(ValueError): @tm.run(-1) def _(): raise AssertionError("Never called") def test_run_stop_on_success(self): tm = self._makeOne() i = [0, None] @tm.run() def meaning(): i[0] += 1 i[1] = tm.get() return 43 self.assertEqual(i[0], 1) self.assertEqual(meaning, 43) self.assertEqual(i[1].description, "meaning") def test_run_retries_but_gives_up(self): import transaction.interfaces class Retry(transaction.interfaces.TransientError): pass tm = self._makeOne() i = [0] with self.assertRaises(Retry): @tm.run() def _(): i[0] += 1 raise Retry self.assertEqual(i[0], 3) def test_run_propigates_errors(self): tm = self._makeOne() with self.assertRaises(ValueError): @tm.run def _(): raise ValueError def test_run_defer_to_dm(self): import transaction.tests.savepointsample class DM(transaction.tests.savepointsample.SampleSavepointDataManager): def should_retry(self, e): if 'should retry' in str(e): return True ntry = [0] dm = transaction.tests.savepointsample.SampleSavepointDataManager() dm2 = DM() with transaction.manager: dm2['ntry'] = 0 @transaction.manager.run def _(): ntry[0] += 1 dm['ntry'] = ntry[0] dm2['ntry'] = ntry[0] if ntry[0] % 3: raise ValueError('we really should retry this') self.assertEqual(ntry[0], 3) def test_run_callable_with_bytes_doc(self): import transaction class Callable: def __init__(self): self.__doc__ = b'some bytes' self.__name__ = b'more bytes' def __call__(self): return 42 result = transaction.manager.run(Callable()) self.assertEqual(result, 42) def test__retryable_w_transient_error(self): from transaction.interfaces import TransientError tm = self._makeOne() self.assertTrue(tm._retryable(TransientError, object())) def test__retryable_w_transient_subclass(self): from transaction.interfaces import TransientError class _Derived(TransientError): pass tm = self._makeOne() self.assertTrue(tm._retryable(_Derived, object())) def test__retryable_w_normal_exception_no_resources(self): tm = self._makeOne() self.assertFalse(tm._retryable(Exception, object())) def test__retryable_w_normal_exception_w_resource_voting_yes(self): class _Resource: def should_retry(self, err): return True tm = self._makeOne() tm.get()._resources.append(_Resource()) self.assertTrue(tm._retryable(Exception, object())) def test__retryable_w_multiple(self): class _Resource: _should = True def should_retry(self, err): return self._should tm = self._makeOne() res1 = _Resource() res1._should = False res2 = _Resource() tm.get()._resources.append(res1) tm.get()._resources.append(res2) self.assertTrue(tm._retryable(Exception, object())) # basic tests with two sub trans jars # really we only need one, so tests for # sub1 should identical to tests for sub2 def test_commit_normal(self): mgr, sub1, sub2, sub3, nosub1 = self._makePopulated() sub1.modify() sub2.modify() mgr.commit() assert sub1._p_jar.ccommit_sub == 0 assert sub1._p_jar.ctpc_finish == 1 def test_abort_normal(self): mgr, sub1, sub2, sub3, nosub1 = self._makePopulated() sub1.modify() sub2.modify() mgr.abort() assert sub2._p_jar.cabort == 1 # repeat adding in a nonsub trans jars def test_commit_w_nonsub_jar(self): mgr, sub1, sub2, sub3, nosub1 = self._makePopulated() nosub1.modify() mgr.commit() assert nosub1._p_jar.ctpc_finish == 1 def test_abort_w_nonsub_jar(self): mgr, sub1, sub2, sub3, nosub1 = self._makePopulated() nosub1.modify() mgr.abort() assert nosub1._p_jar.ctpc_finish == 0 assert nosub1._p_jar.cabort == 1 ### # Failure Mode Tests # # ok now we do some more interesting # tests that check the implementations # error handling by throwing errors from # various jar methods ### # first the recoverable errors def test_abort_w_broken_jar(self): from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): mgr, sub1, sub2, sub3, nosub1 = self._makePopulated() sub1._p_jar = BasicJar(errors='abort') nosub1.modify() sub1.modify(nojar=1) sub2.modify() try: mgr.abort() except TestTxnException: pass assert nosub1._p_jar.cabort == 1 assert sub2._p_jar.cabort == 1 def test_commit_w_broken_jar_commit(self): from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): mgr, sub1, sub2, sub3, nosub1 = self._makePopulated() sub1._p_jar = BasicJar(errors='commit') nosub1.modify() sub1.modify(nojar=1) try: mgr.commit() except TestTxnException: pass assert nosub1._p_jar.ctpc_finish == 0 assert nosub1._p_jar.ccommit == 1 assert nosub1._p_jar.ctpc_abort == 1 def test_commit_w_broken_jar_tpc_vote(self): from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): mgr, sub1, sub2, sub3, nosub1 = self._makePopulated() sub1._p_jar = BasicJar(errors='tpc_vote') nosub1.modify() sub1.modify(nojar=1) try: mgr.commit() except TestTxnException: pass assert nosub1._p_jar.ctpc_finish == 0 assert nosub1._p_jar.ccommit == 1 assert nosub1._p_jar.ctpc_abort == 1 assert sub1._p_jar.ctpc_abort == 1 def test_commit_w_broken_jar_tpc_begin(self): # ok this test reveals a bug in the TM.py # as the nosub tpc_abort there is ignored. # nosub calling method tpc_begin # nosub calling method commit # sub calling method tpc_begin # sub calling method abort # sub calling method tpc_abort # nosub calling method tpc_abort from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): mgr, sub1, sub2, sub3, nosub1 = self._makePopulated() sub1._p_jar = BasicJar(errors='tpc_begin') nosub1.modify() sub1.modify(nojar=1) try: mgr.commit() except TestTxnException: pass assert nosub1._p_jar.ctpc_abort == 1 assert sub1._p_jar.ctpc_abort == 1 def test_commit_w_broken_jar_tpc_abort_tpc_vote(self): from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): mgr, sub1, sub2, sub3, nosub1 = self._makePopulated() sub1._p_jar = BasicJar(errors=('tpc_abort', 'tpc_vote')) nosub1.modify() sub1.modify(nojar=1) try: mgr.commit() except TestTxnException: pass assert nosub1._p_jar.ctpc_abort == 1 def test_notify_transaction_late_comers(self): # If a datamanager registers for synchonization after a # transaction has started, we should call newTransaction so it # can do necessry setup. from unittest import mock from .. import TransactionManager manager = TransactionManager() sync1 = mock.MagicMock() manager.registerSynch(sync1) sync1.newTransaction.assert_not_called() t = manager.begin() sync1.newTransaction.assert_called_with(t) sync2 = mock.MagicMock() manager.registerSynch(sync2) sync2.newTransaction.assert_called_with(t) # for, um, completeness t.commit() for s in sync1, sync2: s.beforeCompletion.assert_called_with(t) s.afterCompletion.assert_called_with(t) def test_unregisterSynch_on_transaction_manager_from_serparate_thread( self): # We should be able to get the underlying manager of the thread manager # and call methods from other threads. import threading import transaction started = threading.Event() stopped = threading.Event() synchronizer = self class Runner(threading.Thread): def __init__(self): threading.Thread.__init__(self) self.manager = transaction.manager.manager self.daemon = True self.start() def run(self): self.manager.registerSynch(synchronizer) started.set() stopped.wait() runner = Runner() started.wait() runner.manager.unregisterSynch(synchronizer) 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): def test_interface(self): import transaction zope.interface.verify.verifyObject(interfaces.ITransactionManager, transaction.manager) def test_sync_registration_thread_local_manager(self): import transaction sync = mock.MagicMock() sync2 = mock.MagicMock() self.assertFalse(transaction.manager.registeredSynchs()) transaction.manager.registerSynch(sync) self.assertTrue(transaction.manager.registeredSynchs()) transaction.manager.registerSynch(sync2) self.assertTrue(transaction.manager.registeredSynchs()) t = transaction.begin() sync.newTransaction.assert_called_with(t) transaction.abort() sync.beforeCompletion.assert_called_with(t) sync.afterCompletion.assert_called_with(t) transaction.manager.unregisterSynch(sync) self.assertTrue(transaction.manager.registeredSynchs()) transaction.manager.unregisterSynch(sync2) self.assertFalse(transaction.manager.registeredSynchs()) sync.reset_mock() transaction.begin() transaction.abort() sync.newTransaction.assert_not_called() sync.beforeCompletion.assert_not_called() sync.afterCompletion.assert_not_called() self.assertFalse(transaction.manager.registeredSynchs()) transaction.manager.registerSynch(sync) transaction.manager.registerSynch(sync2) t = transaction.begin() sync.newTransaction.assert_called_with(t) self.assertTrue(transaction.manager.registeredSynchs()) transaction.abort() sync.beforeCompletion.assert_called_with(t) sync.afterCompletion.assert_called_with(t) transaction.manager.clearSynchs() self.assertFalse(transaction.manager.registeredSynchs()) sync.reset_mock() transaction.begin() transaction.abort() sync.newTransaction.assert_not_called() sync.beforeCompletion.assert_not_called() sync.afterCompletion.assert_not_called() def test_explicit_thread_local_manager(self): import transaction.interfaces self.assertFalse(transaction.manager.explicit) transaction.abort() transaction.manager.explicit = True self.assertTrue(transaction.manager.explicit) with self.assertRaises(transaction.interfaces.NoTransaction): transaction.abort() transaction.manager.explicit = False transaction.abort() class AttemptTests(unittest.TestCase): def _makeOne(self, manager): from transaction._manager import Attempt return Attempt(manager) def test___enter__(self): manager = DummyManager() inst = self._makeOne(manager) inst.__enter__() self.assertTrue(manager.entered) def test___exit__no_exc_no_commit_exception(self): manager = DummyManager() inst = self._makeOne(manager) result = inst.__exit__(None, None, None) self.assertFalse(result) self.assertTrue(manager.committed) def test___exit__no_exc_nonretryable_commit_exception(self): manager = DummyManager(raise_on_commit=ValueError) inst = self._makeOne(manager) self.assertRaises(ValueError, inst.__exit__, None, None, None) self.assertTrue(manager.committed) self.assertTrue(manager.aborted) def test___exit__no_exc_abort_exception_after_nonretryable_commit_exc( self): manager = DummyManager(raise_on_abort=ValueError, raise_on_commit=KeyError) inst = self._makeOne(manager) self.assertRaises(ValueError, inst.__exit__, None, None, None) self.assertTrue(manager.committed) self.assertTrue(manager.aborted) def test___exit__no_exc_retryable_commit_exception(self): from transaction.interfaces import TransientError manager = DummyManager(raise_on_commit=TransientError) inst = self._makeOne(manager) result = inst.__exit__(None, None, None) self.assertTrue(result) self.assertTrue(manager.committed) self.assertTrue(manager.aborted) def test___exit__with_exception_value_retryable(self): from transaction.interfaces import TransientError manager = DummyManager() inst = self._makeOne(manager) result = inst.__exit__(TransientError, TransientError(), None) self.assertTrue(result) self.assertFalse(manager.committed) self.assertTrue(manager.aborted) def test___exit__with_exception_value_nonretryable(self): manager = DummyManager() inst = self._makeOne(manager) self.assertRaises(KeyError, inst.__exit__, KeyError, KeyError(), None) self.assertFalse(manager.committed) self.assertTrue(manager.aborted) def test_explicit_mode(self): from .. import TransactionManager from ..interfaces import AlreadyInTransaction from ..interfaces import NoTransaction tm = TransactionManager() self.assertFalse(tm.explicit) tm = TransactionManager(explicit=True) self.assertTrue(tm.explicit) for name in 'get', 'commit', 'abort', 'doom', 'isDoomed', 'savepoint': with self.assertRaises(NoTransaction): getattr(tm, name)() t = tm.begin() with self.assertRaises(AlreadyInTransaction): tm.begin() self.assertIs(t, tm.get()) self.assertFalse(tm.isDoomed()) tm.doom() self.assertTrue(tm.isDoomed()) tm.abort() for name in 'get', 'commit', 'abort', 'doom', 'isDoomed', 'savepoint': with self.assertRaises(NoTransaction): getattr(tm, name)() t = tm.begin() self.assertFalse(tm.isDoomed()) with self.assertRaises(AlreadyInTransaction): tm.begin() tm.savepoint() tm.commit() class DummyManager: entered = False committed = False aborted = False def __init__(self, raise_on_commit=None, raise_on_abort=None): self.raise_on_commit = raise_on_commit self.raise_on_abort = raise_on_abort def _retryable(self, t, v): from transaction._manager import TransientError return issubclass(t, TransientError) def __enter__(self): self.entered = True def abort(self): self.aborted = True if self.raise_on_abort: raise self.raise_on_abort def commit(self): self.committed = True if self.raise_on_commit: raise self.raise_on_commit class DataObject: def __init__(self, transaction_manager, nost=0): self.transaction_manager = transaction_manager self.nost = nost self._p_jar = None def modify(self, nojar=0, tracing=0): if not nojar: if self.nost: self._p_jar = BasicJar(tracing=tracing) else: self._p_jar = BasicJar(tracing=tracing) self.transaction_manager.get().join(self._p_jar) class TestTxnException(Exception): pass class BasicJar: def __init__(self, errors=(), tracing=0): if not isinstance(errors, tuple): errors = errors, self.errors = errors self.tracing = tracing self.cabort = 0 self.ccommit = 0 self.ctpc_begin = 0 self.ctpc_abort = 0 self.ctpc_vote = 0 self.ctpc_finish = 0 self.cabort_sub = 0 self.ccommit_sub = 0 def __repr__(self): return "<{} {:X} {}>".format( self.__class__.__name__, positive_id(self), self.errors) def sortKey(self): # All these jars use the same sort key, and Python's list.sort() # is stable. These two return self.__class__.__name__ def check(self, method): if self.tracing: # pragma: no cover print(f'{str(self.tracing)} calling method {method}') if method in self.errors: raise TestTxnException("error %s" % method) # basic jar txn interface def abort(self, *args): self.check('abort') self.cabort += 1 def commit(self, *args): self.check('commit') self.ccommit += 1 def tpc_begin(self, txn, sub=0): self.check('tpc_begin') self.ctpc_begin += 1 def tpc_vote(self, *args): self.check('tpc_vote') self.ctpc_vote += 1 def tpc_abort(self, *args): self.check('tpc_abort') self.ctpc_abort += 1 def tpc_finish(self, *args): self.check('tpc_finish') self.ctpc_finish += 1 class DummySynch: def __init__(self): self._txns = set() def newTransaction(self, txn): self._txns.add(txn) def positive_id(obj): """Return id(obj) as a non-negative integer.""" import struct _ADDRESS_MASK = 256 ** struct.calcsize('P') result = id(obj) if result < 0: # pragma: no cover # Happens...on old 32-bit systems? result += _ADDRESS_MASK assert result > 0 return result ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726644116.0 transaction-5.0/src/transaction/tests/test__transaction.py0000644000076500000240000017224614672477624023177 0ustar00jensstaff############################################################################## # # Copyright (c) 2001, 2002, 2005 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE # ############################################################################## """Test transaction behavior for variety of cases. I wrote these unittests to investigate some odd transaction behavior when doing unittests of integrating non sub transaction aware objects, and to insure proper txn behavior. these tests test the transaction system independent of the rest of the zodb. you can see the method calls to a jar by passing the keyword arg tracing to the modify method of a dataobject. the value of the arg is a prefix used for tracing print calls to that objects jar. the number of times a jar method was called can be inspected by looking at an attribute of the jar that is the method name prefixed with a c (count/check). i've included some tracing examples for tests that i thought were illuminating as doc strings below. TODO add in tests for objects which are modified multiple times, for example an object that gets modified in multiple sub txns. """ import os import unittest import warnings class TransactionTests(unittest.TestCase): def _getTargetClass(self): from transaction._transaction import Transaction return Transaction def _makeOne(self, synchronizers=None, manager=None): return self._getTargetClass()(synchronizers, manager) def test_verifyImplements_ITransaction(self): from zope.interface.verify import verifyClass from transaction.interfaces import ITransaction verifyClass(ITransaction, self._getTargetClass()) def test_verifyProvides_ITransaction(self): from zope.interface.verify import verifyObject from transaction.interfaces import ITransaction verifyObject(ITransaction, self._makeOne()) def test_ctor_defaults(self): from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey from transaction.weakset import WeakSet logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): txn = self._makeOne() self.assertIsInstance(txn._synchronizers, WeakSet) self.assertEqual(len(txn._synchronizers), 0) self.assertIsNone(txn._manager) self.assertEqual(txn.user, "") self.assertEqual(txn.description, "") self.assertIsNone(txn._savepoint2index) self.assertEqual(txn._savepoint_index, 0) self.assertEqual(txn._resources, []) self.assertEqual(txn._adapters, {}) self.assertEqual(txn._voted, {}) self.assertEqual(txn.extension, {}) self.assertIs(txn._extension, txn.extension) # legacy self.assertIs(txn.log, logger) self.assertEqual(len(logger._log), 1) self.assertEqual(logger._log[0][0], 'debug') self.assertEqual(logger._log[0][1], 'new transaction') self.assertIsNone(txn._failure_traceback) self.assertEqual(txn._before_commit, []) self.assertEqual(txn._after_commit, []) def test_ctor_w_syncs(self): from transaction.weakset import WeakSet synchs = WeakSet() txn = self._makeOne(synchronizers=synchs) self.assertIs(txn._synchronizers, synchs) def test_isDoomed(self): from transaction._transaction import Status txn = self._makeOne() self.assertFalse(txn.isDoomed()) txn.status = Status.DOOMED self.assertTrue(txn.isDoomed()) def test_doom_active(self): from transaction._transaction import Status txn = self._makeOne() txn.doom() self.assertTrue(txn.isDoomed()) self.assertEqual(txn.status, Status.DOOMED) def test_doom_invalid(self): from transaction._transaction import Status txn = self._makeOne() for status in Status.COMMITTING, Status.COMMITTED, Status.COMMITFAILED: txn.status = status self.assertRaises(ValueError, txn.doom) def test_doom_already_doomed(self): from transaction._transaction import Status txn = self._makeOne() txn.status = Status.DOOMED txn.doom() self.assertTrue(txn.isDoomed()) self.assertEqual(txn.status, Status.DOOMED) def test__prior_operation_failed(self): from transaction.interfaces import TransactionFailedError class _Traceback: def getvalue(self): return 'TRACEBACK' txn = self._makeOne() txn._failure_traceback = _Traceback() with self.assertRaises(TransactionFailedError) as exc: txn._prior_operation_failed() err = exc.exception self.assertTrue(str(err).startswith('An operation previously failed')) self.assertTrue(str(err).endswith("with traceback:\n\nTRACEBACK")) def test_join_COMMITFAILED(self): from transaction._transaction import Status from transaction.interfaces import TransactionFailedError class _Traceback: def getvalue(self): return 'TRACEBACK' txn = self._makeOne() txn.status = Status.COMMITFAILED txn._failure_traceback = _Traceback() self.assertRaises(TransactionFailedError, txn.join, object()) def test_join_COMMITTING(self): from transaction._transaction import Status txn = self._makeOne() txn.status = Status.COMMITTING self.assertRaises(ValueError, txn.join, object()) def test_join_COMMITTED(self): from transaction._transaction import Status txn = self._makeOne() txn.status = Status.COMMITTED self.assertRaises(ValueError, txn.join, object()) def test_join_DOOMED_non_preparing_wo_sp2index(self): from transaction._transaction import Status txn = self._makeOne() txn.status = Status.DOOMED resource = object() txn.join(resource) self.assertEqual(txn._resources, [resource]) def test__unjoin_miss(self): txn = self._makeOne() txn._unjoin(object()) # no raise def test__unjoin_hit(self): txn = self._makeOne() resource = object() txn._resources.append(resource) txn._unjoin(resource) self.assertEqual(txn._resources, []) def test_savepoint_COMMITFAILED(self): from transaction._transaction import Status from transaction.interfaces import TransactionFailedError class _Traceback: def getvalue(self): return 'TRACEBACK' txn = self._makeOne() txn.status = Status.COMMITFAILED txn._failure_traceback = _Traceback() self.assertRaises(TransactionFailedError, txn.savepoint) def test_savepoint_empty(self): from weakref import WeakKeyDictionary from transaction import _transaction from transaction._transaction import Savepoint from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): txn = self._makeOne() sp = txn.savepoint() self.assertIsInstance(sp, Savepoint) self.assertIs(sp.transaction, txn) self.assertEqual(sp._savepoints, []) self.assertEqual(txn._savepoint_index, 1) self.assertIsInstance(txn._savepoint2index, WeakKeyDictionary) self.assertEqual(txn._savepoint2index[sp], 1) def test_savepoint_non_optimistc_resource_wo_support(self): from io import StringIO from transaction import _transaction from transaction._transaction import Status from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): txn = self._makeOne() logger._clear() resource = object() txn._resources.append(resource) self.assertRaises(TypeError, txn.savepoint) self.assertEqual(txn.status, Status.COMMITFAILED) self.assertIsInstance(txn._failure_traceback, StringIO) self.assertIn('TypeError', txn._failure_traceback.getvalue()) self.assertEqual(len(logger._log), 2) self.assertEqual(logger._log[0][0], 'error') self.assertTrue(logger._log[0][1].startswith('Error in abort')) self.assertEqual(logger._log[1][0], 'error') self.assertTrue(logger._log[1][1].startswith('Error in tpc_abort')) def test__remove_and_invalidate_after_miss(self): from weakref import WeakKeyDictionary txn = self._makeOne() txn._savepoint2index = WeakKeyDictionary() class _SP: def __init__(self, txn): self.transaction = txn holdme = [] for i in range(10): sp = _SP(txn) holdme.append(sp) # prevent gc txn._savepoint2index[sp] = i self.assertEqual(len(txn._savepoint2index), 10) self.assertRaises(KeyError, txn._remove_and_invalidate_after, _SP(txn)) self.assertEqual(len(txn._savepoint2index), 10) def test__remove_and_invalidate_after_hit(self): from weakref import WeakKeyDictionary txn = self._makeOne() txn._savepoint2index = WeakKeyDictionary() class _SP: def __init__(self, txn, index): self.transaction = txn self._index = index def __lt__(self, other): return self._index < other._index def __repr__(self): # pragma: no cover return '_SP: %d' % self._index holdme = [] for i in range(10): sp = _SP(txn, i) holdme.append(sp) # prevent gc txn._savepoint2index[sp] = i self.assertEqual(len(txn._savepoint2index), 10) txn._remove_and_invalidate_after(holdme[1]) self.assertEqual(sorted(txn._savepoint2index), sorted(holdme[:2])) def test__invalidate_all_savepoints(self): from weakref import WeakKeyDictionary txn = self._makeOne() txn._savepoint2index = WeakKeyDictionary() class _SP: def __init__(self, txn, index): self.transaction = txn self._index = index def __repr__(self): # pragma: no cover return '_SP: %d' % self._index holdme = [] for i in range(10): sp = _SP(txn, i) holdme.append(sp) # prevent gc txn._savepoint2index[sp] = i self.assertEqual(len(txn._savepoint2index), 10) txn._invalidate_all_savepoints() self.assertEqual(list(txn._savepoint2index), []) def test_commit_DOOMED(self): from transaction._transaction import Status from transaction.interfaces import DoomedTransaction txn = self._makeOne() txn.status = Status.DOOMED self.assertRaises(DoomedTransaction, txn.commit) def test_commit_COMMITFAILED(self): from transaction._transaction import Status from transaction.interfaces import TransactionFailedError class _Traceback: def getvalue(self): return 'TRACEBACK' txn = self._makeOne() txn.status = Status.COMMITFAILED txn._failure_traceback = _Traceback() self.assertRaises(TransactionFailedError, txn.commit) def test_commit_wo_savepoints_wo_hooks_wo_synchronizers(self): from transaction import _transaction from transaction._transaction import Status from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey class _Mgr: def __init__(self, txn): self._txn = txn def free(self, txn): assert txn is self._txn self._txn = None logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): txn = self._makeOne() logger._clear() mgr = txn._manager = _Mgr(txn) txn.commit() self.assertEqual(txn.status, Status.COMMITTED) self.assertIsNone(mgr._txn) self.assertEqual(logger._log[0][0], 'debug') self.assertEqual(logger._log[0][1], 'commit') def test_commit_w_savepoints(self): from weakref import WeakKeyDictionary from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey class _SP: def __init__(self, txn, index): self.transaction = txn self._index = index def __repr__(self): # pragma: no cover return '_SP: %d' % self._index logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): txn = self._makeOne() txn._savepoint2index = WeakKeyDictionary() holdme = [] for i in range(10): sp = _SP(txn, i) holdme.append(sp) # prevent gc txn._savepoint2index[sp] = i logger._clear() txn.commit() self.assertEqual(list(txn._savepoint2index), []) def test_commit_w_beforeCommitHooks(self): from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey _hooked1, _hooked2 = [], [] def _hook1(*args, **kw): _hooked1.append((args, kw)) def _hook2(*args, **kw): _hooked2.append((args, kw)) logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): txn = self._makeOne() txn._before_commit.append((_hook1, ('one',), {'uno': 1})) txn._before_commit.append((_hook2, (), {})) logger._clear() txn.commit() self.assertEqual(_hooked1, [(('one',), {'uno': 1})]) self.assertEqual(_hooked2, [((), {})]) self.assertEqual(txn._before_commit, []) def test_commit_w_synchronizers(self): from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey from transaction.weakset import WeakSet class _Synch: _before = _after = False def beforeCompletion(self, txn): self._before = txn def afterCompletion(self, txn): self._after = txn synchs = [_Synch(), _Synch(), _Synch()] ws = WeakSet() for synch in synchs: ws.add(synch) logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): txn = self._makeOne(synchronizers=ws) logger._clear() txn.commit() for synch in synchs: self.assertIs(synch._before, txn) self.assertIs(synch._after, txn) def test_commit_w_afterCommitHooks(self): from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey _hooked1, _hooked2 = [], [] def _hook1(*args, **kw): _hooked1.append((args, kw)) def _hook2(*args, **kw): _hooked2.append((args, kw)) logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): txn = self._makeOne() txn._after_commit.append((_hook1, ('one',), {'uno': 1})) txn._after_commit.append((_hook2, (), {})) logger._clear() txn.commit() self.assertEqual(_hooked1, [((True, 'one',), {'uno': 1})]) self.assertEqual(_hooked2, [((True,), {})]) self.assertEqual(txn._after_commit, []) self.assertEqual(txn._resources, []) def test_commit_error_w_afterCompleteHooks(self): from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey class BrokenResource: def sortKey(self): return 'zzz' def tpc_begin(self, txn): raise ValueError('test') broken = BrokenResource() resource = Resource('aaa') _hooked1, _hooked2 = [], [] def _hook1(*args, **kw): _hooked1.append((args, kw)) def _hook2(*args, **kw): _hooked2.append((args, kw)) logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): txn = self._makeOne() txn._after_commit.append((_hook1, ('one',), {'uno': 1})) txn._after_commit.append((_hook2, (), {})) txn._resources.append(broken) txn._resources.append(resource) logger._clear() self.assertRaises(ValueError, txn.commit) self.assertEqual(_hooked1, [((False, 'one',), {'uno': 1})]) self.assertEqual(_hooked2, [((False,), {})]) self.assertEqual(txn._after_commit, []) self.assertTrue(resource._b) self.assertFalse(resource._c) self.assertFalse(resource._v) self.assertFalse(resource._f) self.assertTrue(resource._a) self.assertTrue(resource._x) def test_commit_error_w_synchronizers(self): from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey from transaction.weakset import WeakSet class _Synch: _before = _after = False def beforeCompletion(self, txn): self._before = txn def afterCompletion(self, txn): self._after = txn synchs = [_Synch(), _Synch(), _Synch()] ws = WeakSet() for synch in synchs: ws.add(synch) class BrokenResource: def sortKey(self): return 'zzz' def tpc_begin(self, txn): raise ValueError('test') broken = BrokenResource() logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): txn = self._makeOne(synchronizers=ws) logger._clear() txn._resources.append(broken) self.assertRaises(ValueError, txn.commit) for synch in synchs: self.assertIs(synch._before, txn) self.assertIs(synch._after, txn) # called in _cleanup def test_commit_clears_resources(self): class DM: tpc_begin = commit = tpc_finish = tpc_vote = lambda s, txn: True dm = DM() txn = self._makeOne() txn.join(dm) self.assertEqual(txn._resources, [dm]) txn.commit() self.assertEqual(txn._resources, []) def test_getBeforeCommitHooks_empty(self): txn = self._makeOne() self.assertEqual(list(txn.getBeforeCommitHooks()), []) def test_addBeforeCommitHook(self): def _hook(*args, **kw): raise AssertionError("Not called") txn = self._makeOne() txn.addBeforeCommitHook(_hook, ('one',), dict(uno=1)) self.assertEqual(list(txn.getBeforeCommitHooks()), [(_hook, ('one',), {'uno': 1})]) def test_callBeforeCommitHook_w_error(self): from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey _calls = [] def _hook(*args, **kw): _calls.append((args, kw)) def _hook_err(*args, **kw): raise ValueError() logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): txn = self._makeOne() logger._clear() txn.addBeforeCommitHook(_hook, ('one',), dict(uno=1)) txn.addBeforeCommitHook(_hook_err, ('two',), dict(dos=2)) txn.addBeforeCommitHook(_hook, ('three',), dict(tres=3)) # only first hook gets called, and instead of logging the error, # the exception is raised self.assertRaises(ValueError, txn._callBeforeCommitHooks) self.assertEqual(_calls, [(('one',), {'uno': 1})]) self.assertEqual(len(logger._log), 0) def test_addBeforeCommitHook_w_kws(self): def _hook(*args, **kw): raise AssertionError("Not called") txn = self._makeOne() txn.addBeforeCommitHook(_hook, ('one',)) self.assertEqual(list(txn.getBeforeCommitHooks()), [(_hook, ('one',), {})]) def test_getAfterCommitHooks_empty(self): txn = self._makeOne() self.assertEqual(list(txn.getAfterCommitHooks()), []) def test_addAfterCommitHook(self): def _hook(*args, **kw): raise AssertionError("Not called") txn = self._makeOne() txn.addAfterCommitHook(_hook, ('one',), dict(uno=1)) self.assertEqual(list(txn.getAfterCommitHooks()), [(_hook, ('one',), {'uno': 1})]) def test_addAfterCommitHook_wo_kws(self): def _hook(*args, **kw): raise AssertionError("Not called") txn = self._makeOne() txn.addAfterCommitHook(_hook, ('one',)) self.assertEqual(list(txn.getAfterCommitHooks()), [(_hook, ('one',), {})]) def test_callAfterCommitHook_w_error(self): from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey _hooked2 = [] def _hook1(*args, **kw): raise ValueError() def _hook2(*args, **kw): _hooked2.append((args, kw)) logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): txn = self._makeOne() logger._clear() txn.addAfterCommitHook(_hook1, ('one',)) txn.addAfterCommitHook(_hook2, ('two',), dict(dos=2)) txn._callAfterCommitHooks() # second hook gets called even if first raises self.assertEqual(_hooked2, [((True, 'two',), {'dos': 2})]) self.assertEqual(len(logger._log), 1) self.assertEqual(logger._log[0][0], 'error') self.assertTrue(logger._log[0][1].startswith("Error in hook")) def test_callAfterCommitHook_w_abort(self): from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey _hooked2 = [] def _hook1(*args, **kw): raise ValueError() def _hook2(*args, **kw): _hooked2.append((args, kw)) logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): txn = self._makeOne() logger._clear() txn.addAfterCommitHook(_hook1, ('one',)) txn.addAfterCommitHook(_hook2, ('two',), dict(dos=2)) txn._callAfterCommitHooks() self.assertEqual(logger._log[0][0], 'error') self.assertTrue(logger._log[0][1].startswith("Error in hook")) def test__commitResources_normal(self): from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey resources = [Resource('bbb'), Resource('aaa')] logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): txn = self._makeOne() logger._clear() txn._resources.extend(resources) txn._commitResources() self.assertEqual(len(txn._voted), 2) for r in resources: self.assertTrue(r._b and r._c and r._v and r._f) self.assertFalse(r._a and r._x) self.assertIn(id(r), txn._voted) self.assertEqual(len(logger._log), 2) self.assertEqual(logger._log[0][0], 'debug') self.assertEqual(logger._log[0][1], 'commit Resource: aaa') self.assertEqual(logger._log[1][0], 'debug') self.assertEqual(logger._log[1][1], 'commit Resource: bbb') def test__commitResources_error_in_tpc_begin(self): from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey resources = [Resource('bbb', 'tpc_begin'), Resource('aaa')] logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): txn = self._makeOne() logger._clear() txn._resources.extend(resources) self.assertRaises(ValueError, txn._commitResources) for r in resources: if r._key == 'aaa': self.assertTrue(r._b) else: self.assertFalse(r._b) self.assertFalse(r._c and r._v and r._f) self.assertTrue(r._a and r._x) self.assertEqual(len(logger._log), 0) def test__commitResources_error_in_afterCompletion(self): from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey class _Synchronizers: def __init__(self, res): self._res = res def map(self, func): for res in self._res: func(res) resources = [Resource('bbb', 'tpc_begin'), Resource('aaa', 'afterCompletion')] sync = _Synchronizers(resources) logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): txn = self._makeOne(sync) logger._clear() txn._resources.extend(resources) self.assertRaises(ValueError, txn._commitResources) for r in resources: if r._key == 'aaa': self.assertTrue(r._b) else: self.assertFalse(r._b) self.assertFalse(r._c and r._v and r._f) self.assertTrue(r._a and r._x) self.assertEqual(len(logger._log), 0) self.assertTrue(resources[0]._after) self.assertFalse(resources[1]._after) def test__commitResources_error_in_commit(self): from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey resources = [Resource('bbb', 'commit'), Resource('aaa')] logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): txn = self._makeOne() logger._clear() txn._resources.extend(resources) self.assertRaises(ValueError, txn._commitResources) for r in resources: self.assertTrue(r._b) if r._key == 'aaa': self.assertTrue(r._c) else: self.assertFalse(r._c) self.assertFalse(r._v and r._f) self.assertTrue(r._a and r._x) self.assertEqual(len(logger._log), 1) self.assertEqual(logger._log[0][0], 'debug') self.assertEqual(logger._log[0][1], 'commit Resource: aaa') def test__commitResources_error_in_tpc_vote(self): from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey resources = [Resource('bbb', 'tpc_vote'), Resource('aaa')] logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): txn = self._makeOne() logger._clear() txn._resources.extend(resources) self.assertRaises(ValueError, txn._commitResources) self.assertEqual(len(txn._voted), 1) for r in resources: self.assertTrue(r._b and r._c) if r._key == 'aaa': self.assertIn(id(r), txn._voted) self.assertTrue(r._v) self.assertFalse(r._f) self.assertFalse(r._a) self.assertTrue(r._x) else: self.assertNotIn(id(r), txn._voted) self.assertFalse(r._v) self.assertFalse(r._f) self.assertTrue(r._a and r._x) self.assertEqual(len(logger._log), 2) self.assertEqual(logger._log[0][0], 'debug') self.assertEqual(logger._log[0][1], 'commit Resource: aaa') self.assertEqual(logger._log[1][0], 'debug') self.assertEqual(logger._log[1][1], 'commit Resource: bbb') def test__commitResources_error_in_tpc_finish(self): from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey resources = [Resource('bbb', 'tpc_finish'), Resource('aaa')] logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): txn = self._makeOne() logger._clear() txn._resources.extend(resources) self.assertRaises(ValueError, txn._commitResources) for r in resources: self.assertTrue(r._b and r._c and r._v) self.assertIn(id(r), txn._voted) if r._key == 'aaa': self.assertTrue(r._f) else: self.assertFalse(r._f) self.assertFalse(r._a and r._x) # no cleanup if tpc_finish raises self.assertEqual(len(logger._log), 3) self.assertEqual(logger._log[0][0], 'debug') self.assertEqual(logger._log[0][1], 'commit Resource: aaa') self.assertEqual(logger._log[1][0], 'debug') self.assertEqual(logger._log[1][1], 'commit Resource: bbb') self.assertEqual(logger._log[2][0], 'critical') self.assertTrue(logger._log[2][1].startswith( 'A storage error occurred')) def test_abort_wo_savepoints_wo_hooks_wo_synchronizers(self): from transaction import _transaction from transaction._transaction import Status from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey class _Mgr: def __init__(self, txn): self._txn = txn def free(self, txn): assert txn is self._txn self._txn = None logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): txn = self._makeOne() logger._clear() mgr = txn._manager = _Mgr(txn) txn.abort() self.assertEqual(txn.status, Status.ACTIVE) self.assertIsNone(mgr._txn) self.assertEqual(logger._log[0][0], 'debug') self.assertEqual(logger._log[0][1], 'abort') def test_abort_w_savepoints(self): from weakref import WeakKeyDictionary from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey class _SP: def __init__(self, txn, index): self.transaction = txn self._index = index def __repr__(self): # pragma: no cover return '_SP: %d' % self._index logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): txn = self._makeOne() txn._savepoint2index = WeakKeyDictionary() holdme = [] for i in range(10): sp = _SP(txn, i) holdme.append(sp) # prevent gc txn._savepoint2index[sp] = i logger._clear() txn.abort() self.assertEqual(list(txn._savepoint2index), []) def test_abort_w_beforeCommitHooks(self): from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey _hooked1, _hooked2 = [], [] def _hook1(*args, **kw): raise AssertionError("Not called") def _hook2(*args, **kw): raise AssertionError("Not called") logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): txn = self._makeOne() txn._before_commit.append((_hook1, ('one',), {'uno': 1})) txn._before_commit.append((_hook2, (), {})) logger._clear() txn.abort() self.assertEqual(_hooked1, []) self.assertEqual(_hooked2, []) # Hooks are not called but cleared on abort self.assertEqual(list(txn.getBeforeCommitHooks()), []) self.assertIsNone(txn._manager) def test_abort_w_synchronizers(self): from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey test = self class _Synch: _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 # 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: synchs = [_Synch(), _Synch(), _Synch(), _BadSynch()] def map(self, func): for s in self.synchs: func(s) logger = DummyLogger() class Manager: 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=synchs, manager=manager) manager.txn = txn logger._clear() 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 import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey _hooked1, _hooked2 = [], [] def _hook1(*args, **kw): raise AssertionError("Not called") def _hook2(*args, **kw): raise AssertionError("Not called") logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): txn = self._makeOne() txn._after_commit.append((_hook1, ('one',), {'uno': 1})) txn._after_commit.append((_hook2, (), {})) logger._clear() txn.abort() # Hooks are not called but cleared on abort self.assertEqual(_hooked1, []) self.assertEqual(_hooked2, []) self.assertEqual(list(txn.getAfterCommitHooks()), []) self.assertEqual(txn._resources, []) self.assertIsNone(txn._manager) def test_abort_error_w_afterCommitHooks(self): from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey class BrokenResource: def sortKey(self): raise AssertionError("Not called") def abort(self, txn): raise ValueError('test') broken = BrokenResource() aaa = Resource('aaa') broken2 = BrokenResource() _hooked1, _hooked2 = [], [] def _hook1(*args, **kw): raise AssertionError("Not called") def _hook2(*args, **kw): raise AssertionError("Not called") logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): txn = self._makeOne() txn._after_commit.append((_hook1, ('one',), {'uno': 1})) txn._after_commit.append((_hook2, (), {})) txn._resources.append(aaa) txn._resources.append(broken) txn._resources.append(broken2) logger._clear() self.assertRaises(ValueError, txn.abort) # Hooks are not called but cleared on abort self.assertEqual(_hooked1, []) self.assertEqual(_hooked2, []) 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 import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey from transaction.weakset import WeakSet class _Synch: _before = _after = False def beforeCompletion(self, txn): self._before = txn def afterCompletion(self, txn): self._after = txn synchs = [_Synch(), _Synch(), _Synch()] ws = WeakSet() for synch in synchs: ws.add(synch) class BrokenResource: def sortKey(self): raise AssertionError("Should not be called") def abort(self, txn): raise ValueError('test') broken = BrokenResource() logger = DummyLogger() with Monkey(_transaction, _LOGGER=logger): t = self._makeOne(synchronizers=ws) logger._clear() t._resources.append(broken) self.assertRaises(ValueError, t.abort) for synch in synchs: self.assertIs(synch._before, t) self.assertIs(synch._after, t) # called in _cleanup self.assertIsNot(t._synchronizers, ws) def test_abort_synchronizer_error_w_resources(self): from transaction import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey class _Synch: _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: 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.assertIs(synch._before, t) self.assertIs(synch._after, t) # called in _cleanup self.assertIsNot(t._synchronizers, synchs) self.assertTrue(resource._a) def test_abort_clears_resources(self): class DM: def abort(self, txn): return True dm = DM() txn = self._makeOne() txn.join(dm) self.assertEqual(txn._resources, [dm]) 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 import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey _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 import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey _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 import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey _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 import _transaction from transaction.tests.common import DummyLogger from transaction.tests.common import Monkey _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: txn.note('This is a note.') self.assertEqual(txn.description, 'This is a note.') txn.note('Another.') self.assertEqual(txn.description, 'This is a note.\nAnother.') finally: txn.abort() def test_note_bytes(self): txn = self._makeOne() with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") txn.note(b'haha') self.assertNonTextDeprecationWarning(w) self.assertEqual(txn.description, 'haha') def test_note_None(self): txn = self._makeOne() self.assertEqual('', txn.description) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") txn.note(None) self.assertFalse(w) self.assertEqual(txn.description, '') def test_note_42(self): txn = self._makeOne() with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") txn.note(42) self.assertNonTextDeprecationWarning(w) self.assertEqual(txn.description, '42') def assertNonTextDeprecationWarning(self, w): [w] = w self.assertEqual( (DeprecationWarning, "Expected text", os.path.splitext(__file__)[0]), (w.category, str(w.message), os.path.splitext(w.filename)[0]), ) def test_description_bytes(self): txn = self._makeOne() with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") txn.description = b'haha' self.assertNonTextDeprecationWarning(w) self.assertEqual(txn.description, 'haha') def test_description_42(self): txn = self._makeOne() with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") txn.description = 42 self.assertNonTextDeprecationWarning(w) self.assertEqual(txn.description, '42') def test_description_None(self): txn = self._makeOne() self.assertEqual('', txn.description) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") txn.description = None self.assertFalse(w) self.assertEqual(txn.description, '') def test_setUser_default_path(self): txn = self._makeOne() txn.setUser('phreddy') self.assertEqual(txn.user, '/ phreddy') def test_setUser_explicit_path(self): txn = self._makeOne() txn.setUser('phreddy', '/bedrock') self.assertEqual(txn.user, '/bedrock phreddy') def test_user_w_none(self): txn = self._makeOne() txn.user = 'phreddy' with self.assertRaises(ValueError): txn.user = None # raises self.assertEqual(txn.user, 'phreddy') def _test_user_non_text(self, user, path, expect, both=False): txn = self._makeOne() with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") if path: txn.setUser(user, path) else: if path is None: txn.setUser(user) else: txn.user = user if both: self.assertNonTextDeprecationWarning(w[:1]) self.assertNonTextDeprecationWarning(w[1:]) else: self.assertNonTextDeprecationWarning(w) self.assertEqual(expect, txn.user) def test_user_non_text(self, user=b'phreddy', path=b'/bedrock', expect="/bedrock phreddy", both=True): self._test_user_non_text(b'phreddy', b'/bedrock', "/bedrock phreddy", True) self._test_user_non_text(b'phreddy', None, '/ phreddy') self._test_user_non_text(b'phreddy', False, 'phreddy') self._test_user_non_text(b'phreddy', '/bedrock', '/bedrock phreddy') self._test_user_non_text('phreddy', b'/bedrock', '/bedrock phreddy') self._test_user_non_text('phreddy', 2, '2 phreddy') self._test_user_non_text(1, '/bedrock', '/bedrock 1') self._test_user_non_text(1, 2, '2 1', True) def test_setExtendedInfo_single(self): txn = self._makeOne() txn.setExtendedInfo('frob', 'qux') self.assertEqual(txn.extension, {'frob': 'qux'}) self.assertIs(txn._extension, txn._extension) # legacy def test_setExtendedInfo_multiple(self): txn = self._makeOne() txn.setExtendedInfo('frob', 'qux') txn.setExtendedInfo('baz', 'spam') txn.setExtendedInfo('frob', 'quxxxx') self.assertEqual(txn._extension, {'frob': 'quxxxx', 'baz': 'spam'}) self.assertIs(txn._extension, txn._extension) # legacy def test__extension_settable(self): # Because ZEO sets it. I'll fix ZEO, but maybe something else will # break txn = self._makeOne() txn._extension = dict(baz='spam') txn.setExtendedInfo('frob', 'qux') self.assertEqual(txn.extension, {'frob': 'qux', 'baz': 'spam'}) def test_data(self): txn = self._makeOne() # Can't get data that wasn't set: with self.assertRaises(KeyError) as c: txn.data(self) self.assertEqual(c.exception.args, (self,)) data = dict(a=1) txn.set_data(self, data) self.assertEqual(txn.data(self), data) # Can't get something we haven't stored. with self.assertRaises(KeyError) as c: txn.data(data) self.assertEqual(c.exception.args, (data,)) # When the transaction ends, data are discarded: txn.commit() with self.assertRaises(KeyError) as c: txn.data(self) self.assertEqual(c.exception.args, (self,)) def test_isRetryableError_w_transient_error(self): from transaction._manager import TransactionManager from transaction.interfaces import TransientError txn = self._makeOne(manager=TransactionManager()) txn._manager._txn = txn self.assertTrue(txn.isRetryableError(TransientError())) def test_isRetryableError_w_transient_subclass(self): from transaction._manager import TransactionManager from transaction.interfaces import TransientError class _Derived(TransientError): pass txn = self._makeOne(manager=TransactionManager()) txn._manager._txn = txn self.assertTrue(txn.isRetryableError(_Derived())) def test_isRetryableError_w_normal_exception_no_resources(self): from transaction._manager import TransactionManager txn = self._makeOne(manager=TransactionManager()) txn._manager._txn = txn self.assertFalse(txn.isRetryableError(Exception())) def test_isRetryableError_w_normal_exception_w_resource_voting_yes(self): from transaction._manager import TransactionManager class _Resource: def should_retry(self, err): return True txn = self._makeOne(manager=TransactionManager()) txn._manager._txn = txn txn._resources.append(_Resource()) self.assertTrue(txn.isRetryableError(Exception())) def test_isRetryableError_w_multiple(self): from transaction._manager import TransactionManager class _Resource: _should = True def should_retry(self, err): return self._should txn = self._makeOne(manager=TransactionManager()) txn._manager._txn = txn res1 = _Resource() res1._should = False res2 = _Resource() txn._resources.append(res1) txn._resources.append(res2) self.assertTrue(txn.isRetryableError(Exception())) class Test_rm_key(unittest.TestCase): def _callFUT(self, oid): from transaction._transaction import rm_key return rm_key(oid) def test_miss(self): self.assertIsNone(self._callFUT(object())) def test_hit(self): self.assertEqual(self._callFUT(Resource('zzz')), 'zzz') class SavepointTests(unittest.TestCase): def _getTargetClass(self): from transaction._transaction import Savepoint return Savepoint def _makeOne(self, txn, optimistic, *resources): return self._getTargetClass()(txn, optimistic, *resources) def test_ctor_w_savepoint_oblivious_resource_non_optimistic(self): txn = object() resource = object() self.assertRaises(TypeError, self._makeOne, txn, False, resource) def test_ctor_w_savepoint_oblivious_resource_optimistic(self): from transaction._transaction import NoRollbackSavepoint txn = object() resource = object() sp = self._makeOne(txn, True, resource) self.assertEqual(len(sp._savepoints), 1) self.assertIsInstance(sp._savepoints[0], NoRollbackSavepoint) self.assertIs(sp._savepoints[0].datamanager, resource) def test_ctor_w_savepoint_aware_resources(self): class _Aware: def savepoint(self): return self txn = object() one = _Aware() another = _Aware() sp = self._makeOne(txn, True, one, another) self.assertEqual(len(sp._savepoints), 2) self.assertIsInstance(sp._savepoints[0], _Aware) self.assertIs(sp._savepoints[0], one) self.assertIsInstance(sp._savepoints[1], _Aware) self.assertIs(sp._savepoints[1], another) def test_valid_wo_transacction(self): sp = self._makeOne(None, True, object()) self.assertFalse(sp.valid) def test_valid_w_transacction(self): sp = self._makeOne(object(), True, object()) self.assertTrue(sp.valid) def test_rollback_w_txn_None(self): from transaction.interfaces import InvalidSavepointRollbackError txn = None class _Aware: def savepoint(self): return self resource = _Aware() sp = self._makeOne(txn, False, resource) self.assertRaises(InvalidSavepointRollbackError, sp.rollback) def test_rollback_w_sp_error(self): class _TXN: _sarce = False _raia = None def _saveAndRaiseCommitishError(self): import sys self._sarce = True _, v, tb = sys.exc_info() raise v.with_traceback(tb) def _remove_and_invalidate_after(self, sp): self._raia = sp class _Broken: def rollback(self): raise ValueError() _broken = _Broken() class _GonnaRaise: def savepoint(self): return _broken txn = _TXN() resource = _GonnaRaise() sp = self._makeOne(txn, False, resource) self.assertRaises(ValueError, sp.rollback) self.assertIs(txn._raia, sp) self.assertTrue(txn._sarce) class AbortSavepointTests(unittest.TestCase): def _getTargetClass(self): from transaction._transaction import AbortSavepoint return AbortSavepoint def _makeOne(self, datamanager, transaction): return self._getTargetClass()(datamanager, transaction) def test_ctor(self): dm = object() txn = object() asp = self._makeOne(dm, txn) self.assertIs(asp.datamanager, dm) self.assertIs(asp.transaction, txn) def test_rollback(self): class _DM: _aborted = None def abort(self, txn): self._aborted = txn class _TXN: _unjoined = None def _unjoin(self, datamanager): self._unjoin = datamanager dm = _DM() txn = _TXN() asp = self._makeOne(dm, txn) asp.rollback() self.assertIs(dm._aborted, txn) self.assertIs(txn._unjoin, dm) class NoRollbackSavepointTests(unittest.TestCase): def _getTargetClass(self): from transaction._transaction import NoRollbackSavepoint return NoRollbackSavepoint def _makeOne(self, datamanager): return self._getTargetClass()(datamanager) def test_ctor(self): dm = object() nrsp = self._makeOne(dm) self.assertIs(nrsp.datamanager, dm) def test_rollback(self): dm = object() nrsp = self._makeOne(dm) self.assertRaises(TypeError, nrsp.rollback) class MiscellaneousTests(unittest.TestCase): def test_bug239086(self): # The original implementation of thread transaction manager made # invalid assumptions about thread ids. import threading import transaction import transaction.tests.savepointsample as SPS dm = SPS.SampleSavepointDataManager() self.assertEqual(list(dm.keys()), []) class Sync: def __init__(self, label): self.label = label self.log = [] def beforeCompletion(self, txn): raise AssertionError("Not called") def afterCompletion(self, txn): raise AssertionError("Not called") def newTransaction(self, txn): self.log.append('{} {}'.format(self.label, 'new')) def run_in_thread(f): txn = threading.Thread(target=f) txn.start() txn.join() sync = Sync(1) @run_in_thread def _(): transaction.manager.registerSynch(sync) transaction.manager.begin() dm['a'] = 1 self.assertEqual(sync.log, ['1 new']) @run_in_thread def _2(): transaction.abort() # should do nothing. self.assertEqual(sync.log, ['1 new']) self.assertEqual(list(dm.keys()), ['a']) dm = SPS.SampleSavepointDataManager() self.assertEqual(list(dm.keys()), []) @run_in_thread def _3(): dm['a'] = 1 self.assertEqual(sync.log, ['1 new']) transaction.abort() # should do nothing self.assertEqual(list(dm.keys()), ['a']) def test_gh5(self): from transaction import _transaction buffer = _transaction._makeTracebackBuffer() s = 'ąčę' buffer.write(s) buffer.seek(0) self.assertEqual(buffer.read(), s) class Resource: _b = _c = _v = _f = _a = _x = _after = False def __init__(self, key, error=None): self._key = key self._error = error def __repr__(self): return 'Resource: %s' % self._key def sortKey(self): return self._key def tpc_begin(self, txn): if self._error == 'tpc_begin': raise ValueError() self._b = True def commit(self, txn): if self._error == 'commit': raise ValueError() self._c = True def tpc_vote(self, txn): if self._error == 'tpc_vote': raise ValueError() self._v = True def tpc_finish(self, txn): if self._error == 'tpc_finish': raise ValueError() self._f = True def abort(self, txn): if self._error == 'abort': raise AssertionError("Not called in that state") self._a = True def tpc_abort(self, txn): if self._error == 'tpc_abort': raise AssertionError("Not called in that state") self._x = True def afterCompletion(self, txn): if self._error == 'afterCompletion': raise ValueError() self._after = True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726644116.0 transaction-5.0/src/transaction/tests/test_savepoint.py0000644000076500000240000000435514672477624022516 0ustar00jensstaff############################################################################## # # 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. # ############################################################################## import unittest class SavepointTests(unittest.TestCase): def testRollbackRollsbackDataManagersThatJoinedLater(self): # A savepoint needs to not just rollback it's savepoints, but needs # to # rollback savepoints for data managers that joined savepoints # after the savepoint: import transaction from transaction.tests import savepointsample dm = savepointsample.SampleSavepointDataManager() dm['name'] = 'bob' sp1 = transaction.savepoint() dm['job'] = 'geek' transaction.savepoint() dm['salary'] = 'fun' dm2 = savepointsample.SampleSavepointDataManager() dm2['name'] = 'sally' self.assertIn('name', dm) self.assertIn('job', dm) self.assertIn('salary', dm) self.assertIn('name', dm2) sp1.rollback() self.assertIn('name', dm) self.assertNotIn('job', dm) self.assertNotIn('salary', dm) self.assertNotIn('name', dm2) def test_commit_after_rollback_for_dm_that_joins_after_savepoint(self): # There was a problem handling data managers that joined after a # savepoint. If the savepoint was rolled back and then changes # made, the dm would end up being joined twice, leading to extra # tpc calls and pain. import transaction from transaction.tests import savepointsample sp = transaction.savepoint() dm = savepointsample.SampleSavepointDataManager() dm['name'] = 'bob' sp.rollback() dm['name'] = 'Bob' transaction.commit() self.assertEqual(dm['name'], 'Bob') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726644116.0 transaction-5.0/src/transaction/tests/test_weakset.py0000644000076500000240000000745114672477624022151 0ustar00jensstaff############################################################################## # # Copyright (c) 2007 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE # ############################################################################## import sys import unittest class WeakSetTests(unittest.TestCase): def test_contains(self): from transaction.weakset import WeakSet w = WeakSet() dummy = Dummy() w.add(dummy) self.assertEqual(dummy in w, True) dummy2 = Dummy() self.assertEqual(dummy2 in w, False) def test_len(self): import gc from transaction.weakset import WeakSet w = WeakSet() d1 = Dummy() d2 = Dummy() w.add(d1) w.add(d2) self.assertEqual(len(w), 2) del d1 gc.collect() if sys.platform.startswith('java'): # The Jython GC is non deterministic pass # pragma: no cover else: self.assertEqual(len(w), 1) def test_remove(self): from transaction.weakset import WeakSet w = WeakSet() dummy = Dummy() w.add(dummy) self.assertEqual(dummy in w, True) w.remove(dummy) self.assertEqual(dummy in w, False) def test_clear(self): from transaction.weakset import WeakSet w = WeakSet() dummy = Dummy() w.add(dummy) dummy2 = Dummy() w.add(dummy2) self.assertEqual(dummy in w, True) self.assertEqual(dummy2 in w, True) w.clear() self.assertEqual(dummy in w, False) self.assertEqual(dummy2 in w, False) def test_as_weakref_list(self): import gc from transaction.weakset import WeakSet w = WeakSet() dummy = Dummy() dummy2 = Dummy() dummy3 = Dummy() w.add(dummy) w.add(dummy2) w.add(dummy3) del dummy3 gc.collect() refs = w.as_weakref_list() self.assertIsInstance(refs, list) L = [x() for x in refs] # L is a list, but it does not have a guaranteed order. self.assertTrue(list, type(L)) self.assertEqual(set(L), {dummy, dummy2}) def test_map(self): from transaction.weakset import WeakSet w = WeakSet() dummy = Dummy() dummy2 = Dummy() dummy3 = Dummy() w.add(dummy) w.add(dummy2) w.add(dummy3) def poker(x): x.poked = 1 w.map(poker) for thing in dummy, dummy2, dummy3: self.assertEqual(thing.poked, 1) def test_map_w_gced_element(self): import gc from transaction.weakset import WeakSet w = WeakSet() dummy = Dummy() dummy2 = Dummy() dummy3 = [Dummy()] w.add(dummy) w.add(dummy2) w.add(dummy3[0]) _orig = w.as_weakref_list def _as_weakref_list(): # simulate race condition during iteration of list # object is collected after being iterated. result = _orig() del dummy3[:] gc.collect() return result w.as_weakref_list = _as_weakref_list def poker(x): x.poked = 1 w.map(poker) for thing in dummy, dummy2: self.assertEqual(thing.poked, 1) class Dummy: pass def test_suite(): return unittest.defaultTestLoader.loadTestsFromTestCase(WeakSetTests) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1718202491.0 transaction-5.0/src/transaction/weakset.py0000644000076500000240000000667214632330173017733 0ustar00jensstaff############################################################################ # # Copyright (c) 2007 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################ import weakref # A simple implementation of weak sets, supplying just enough of Python's # sets.Set interface for our needs. class WeakSet: """A set of objects that doesn't keep its elements alive. The objects in the set must be weakly referencable. The objects need not be hashable, and need not support comparison. Two objects are considered to be the same iff their id()s are equal. When the only references to an object are weak references (including those from WeakSets), the object can be garbage-collected, and will vanish from any WeakSets it may be a member of at that time. """ def __init__(self): # Map id(obj) to obj. By using ids as keys, we avoid requiring # that the elements be hashable or comparable. self.data = weakref.WeakValueDictionary() def __len__(self): return len(self.data) def __contains__(self, obj): return id(obj) in self.data # Same as a Set, add obj to the collection. def add(self, obj): self.data[id(obj)] = obj # Same as a Set, remove obj from the collection, and raise # KeyError if obj not in the collection. def remove(self, obj): del self.data[id(obj)] def clear(self): self.data.clear() # f is a one-argument function. Execute f(elt) for each elt in the # set. f's return value is ignored. def map(self, f): for wr in self.as_weakref_list(): elt = wr() if elt is not None: f(elt) # Return a list of weakrefs to all the objects in the collection. # Because a weak dict is used internally, iteration is dicey (the # underlying dict may change size during iteration, due to gc or # activity from other threads). as_weakef_list() is safe. # # If we invoke self.data.values() instead, we get back a list of live # objects instead of weakrefs. If gc occurs while this list is alive, # all the objects move to an older generation (because they're strongly # referenced by the list!). They can't get collected then, until a # less frequent collection of the older generation. Before then, if we # invoke self.data.values() again, they're still alive, and if gc occurs # while that list is alive they're all moved to yet an older generation. # And so on. Stress tests showed that it was easy to get into a state # where a WeakSet grows without bounds, despite that almost all its # elements are actually trash. By returning a list of weakrefs instead, # we avoid that, although the decision to use weakrefs is now very # visible to our clients. def as_weakref_list(self): # The docstring of WeakValueDictionary.valuerefs() # guarantees to return an actual list on all supported versions # of Python. return self.data.valuerefs() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1726644280.5433948 transaction-5.0/src/transaction.egg-info/0000755000076500000240000000000014672500071017375 5ustar00jensstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726644280.0 transaction-5.0/src/transaction.egg-info/PKG-INFO0000644000076500000240000003407614672500070020503 0ustar00jensstaffMetadata-Version: 2.1 Name: transaction Version: 5.0 Summary: Transaction management for Python Home-page: https://github.com/zopefoundation/transaction Author: Zope Foundation and Contributors Author-email: zodb-dev@zope.dev License: ZPL 2.1 Project-URL: Issue Tracker, https://github.com/zopefoundation/transaction/issues Project-URL: Sources, https://github.com/zopefoundation/transaction Platform: any Classifier: Development Status :: 6 - Mature Classifier: License :: OSI Approved :: Zope Public License Classifier: Programming Language :: Python Classifier: Topic :: Database Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: Unix Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Framework :: ZODB Requires-Python: >=3.8 Description-Content-Type: text/x-rst License-File: LICENSE.txt Requires-Dist: zope.interface Provides-Extra: docs Requires-Dist: Sphinx; extra == "docs" Requires-Dist: repoze.sphinx.autointerface; extra == "docs" Provides-Extra: testing Requires-Dist: coverage; extra == "testing" ============ Transactions ============ .. image:: https://github.com/zopefoundation/transaction/actions/workflows/tests.yml/badge.svg :target: https://github.com/zopefoundation/transaction/actions/workflows/tests.yml .. image:: https://readthedocs.org/projects/transaction/badge/?version=latest :target: http://transaction.readthedocs.org/en/latest/ :alt: Documentation Status .. image:: https://img.shields.io/pypi/v/transaction.svg :target: https://pypi.python.org/pypi/transaction :alt: PyPI .. image:: https://img.shields.io/pypi/pyversions/transaction.svg :target: https://pypi.python.org/pypi/transaction :alt: Python versions This package contains a generic transaction implementation for Python. It is mainly used by the ZODB. See http://transaction.readthedocs.org/en/latest for narrative documentation on its usage. ========= Changes ========= 5.0 (2024-09-18) ================ - Add final support for Python 3.13. - Drop support for Python 3.7. 4.0 (2023-11-13) ================ - Drop support for Python 2.7, 3.5, 3.6. - Drop support for deprecated ``python setup.py test.``. - Add support for Python 3.12. - Add preliminary support for Python 3.13a2. 3.1.0 (2023-03-17) ================== - Add support for Python 3.9, 3.10, 3.11. 3.0.1 (2020-12-11) ================== - Exception raised by a before commit hook is no longer hidden. No further commit hooks are called and exception is propagated to the caller of ``commit()``. See `#95 `_. 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) ================== - Changed the implementation of ThreadTransactionManager to be a thread.local that wraps a TransactionManager rather than a thread.local that inherits from TransactionManager. It now exposes a manager attribute that allows access to the wrapped transaction manager to allow cross thread calls. See `issue 68 `_. 2.3.0 (2018-10-19) ================== - Add support for Python 3.7. - Reach 100% test coverage. - Fix ``transaction.manager.run`` formatting transaction notes when given a mix of byte and text strings, such as can happen in Python 2 with ``unicode_literals``. 2.2.1 (2018-03-27) ================== - Make documentation index more user friendly; move old docs to developer section. - Don't crash when printing tracebacks in IPython on Python 2. (This addresses https://github.com/zopefoundation/transaction/issues/5.) 2.2.0 (2018-02-27) ================== - Add support for Python 3.6. - Drop support for Python 3.3. - Add ``isRetryableError`` to the ``transaction.interfaces.ITransaction`` interface to allow external systems to query whether an exception is retryable (transient) by any of the attached data managers. Any ``transaction.interfaces.TransientError`` is considered retryable but a data manager may also consider other exceptions on a per-instance basis. See https://github.com/zopefoundation/transaction/pull/38 2.1.2 (2017-03-11) ================== - To avoid leaking memory, don't include unexpected value in warnings about non-text transaction meta data. 2.1.1 (2017-03-11) ================== - For backward compatibility, relax the requirements that transaction meta data (user or description) be text: - If None is assigned, the assignment is ignored. - If a non-text value is assigned, a warning is issued and the value is converted to text. If the value is a binary string, it will be decoded with the UTF-8 encoding the ``replace`` error policy. 2.1.0 (2017-02-08) ================== Added a transaction-manager explicit mode. Explicit mode makes some kinds of application bugs easier to detect and potentially allows data managers to manage resources more efficiently. (This addresses https://github.com/zopefoundation/transaction/issues/35.) 2.0.3 (2016-11-17) ================== - The user and description fields must now be set with text (unicode) data. Previously, if bytes were provided, they'd be decoded as ASCII. It was decided that this would lead to bugs that were hard to test for. Also, the transaction meta-data field, ``extended_info`` has been renamed to ``extension``. 2.0.2 (2016-11-13) ================== - Fixed: Some legacy applications expect the transaction _extension attribute to be mutable and it wasn't. 2.0.1 (2016-11-11) ================== - The transaction ``user`` and ``description`` attributes are now defined to be text (unicode) as opposed to Python the ``str`` type. - Added the ``extended_info`` transaction attribute which contains transaction meta data. (The ``_extension`` attribute is retained as an alias for backward compatibility.) The transaction interface, ``ITransaction``, now requires ``extended_info`` keys to be text (unicode) and values to be JSON-serializable. - Removed setUser from ITransaction. We'll keep the method indefinitely, but it's unseemly in ITransaction. :) The main purpose of these changes is to tighten up the text specification of user, description and extended_info keys, and to give us more flexibility in the future for serializing extended info. It's possible that these changes will be breaking, so we're also increasing the major version number. 1.7.0 (2016-11-08) ================== - Added a transaction-manager ``run`` method for running a function as a transaction, retrying as necessary on transient errors. - Fixed the transaction manager ``attempts`` method. It didn't stop repeating when there wasn't an error. - Corrected ITransaction by removing beforeCommitHook (which is no longer implemented) and removing 'self' from two methods. 1.6.1 (2016-06-10) ================== - Fixed: Synchonizers that registered with transaction managers when transactions were in progress didn't have their newTransaction methods called to let them know of the in-progress transactions. 1.6.0 (2016-05-21) ================== - New transaction API for storing data on behalf of objects, such as data managers. - Drop references to data managers joined to a transaction when it is committed or aborted. 1.5.0 (2016-05-05) ================== - Drop support for Python 2.6 and 3.2. - Add support for Python 3.5. - Added APIs for interogating and clearing internal state to support client tests. 1.4.4 (2015-05-19) ================== - Use the standard ``valuerefs()`` method rather than relying on implementation details of ``WeakValueDictionary`` in ``WeakSet``. - Add support for PyPy3. - Require 100% branch coverage (in addition to 100% statement coverage). 1.4.3 (2014-03-20) ================== - Add support for Python 3.4. 1.4.2 (skipped) =============== - Released in error as 1.4.3. 1.4.1 (2013-02-20) ================== - Document that values returned by ``sortKey`` must be strings, in order to guarantee total ordering. - Fix occasional RuntimeError: dictionary changed size during iteration errors in transaction.weakset on Python 3. 1.4.0 (2013-01-03) ================== - Updated Trove classifiers. 1.4.0b1 (2012-12-18) ==================== - Converted existing doctests into Sphinx documentation (snippets are exercised via 'tox'). - 100% unit test coverage. - Backward incompatibility: raise ValueError rather than AssertionError for runtime errors: - In ``Transaction.doom`` if the transaction is in a non-doomable state. - In ``TransactionManager.attempts`` if passed a non-positive value. - In ``TransactionManager.free`` if passed a foreign transaction. - Declared support for Python 3.3 in ``setup.py``, and added ``tox`` testing. - When a non-retryable exception was raised as the result of a call to ``transaction.manager.commit`` within the "attempts" machinery, the exception was not reraised properly. Symptom: an unrecoverable exception such as ``Unsupported: Storing blobs in is not supported.`` would be swallowed inappropriately. 1.3.0 (2012-05-16) ================== - Added Sphinx API docuementation. - Added explicit support for PyPy. - Dropped use of Python3-impatible ``zope.interface.implements`` class advisor in favor of ``zope.interface.implementer`` class decorator. - Added support for continuous integration using ``tox`` and ``jenkins``. - Added ``setup.py docs`` alias (installs ``Sphinx`` and dependencies). - Added ``setup.py dev`` alias (runs ``setup.py develop`` plus installs ``nose`` and ``coverage``). - Python 3.3 compatibility. - Fix "for attempt in transaction.attempts(x)" machinery, which would not retry a transaction if its implicit call to ``.commit()`` itself raised a transient error. Symptom: seeing conflict errors even though you thought you were retrying some number of times via the "attempts" machinery (the first attempt to generate an exception during commit would cause that exception to be raised). 1.2.0 (2011-12-05) ================== New Features: - Python 3.2 compatibility. - Dropped Python 2.4 and 2.5 compatibility (use 1.1.1 if you need to use "transaction" under these Python versions). 1.1.1 (2010-09-16) ================== Bug Fixes: - Code in ``_transaction.py`` held on to local references to traceback objects after calling ``sys.exc_info()`` to get one, causing potential reference leakages. - Fixed ``hexlify`` NameError in ``transaction._transaction.oid_repr`` and add test. 1.1.0 (1010-05-12) ================== New Features: - Transaction managers and the transaction module can be used with the with statement to define transaction boundaries, as in:: with transaction: ... do some things ... See transaction/tests/convenience.txt for more details. - There is a new iterator function that automates dealing with transient errors (such as ZODB confict errors). For example, in:: for attempt in transaction.attempts(5): with attempt: ... do some things .. If the work being done raises transient errors, the transaction will be retried up to 5 times. See transaction/tests/convenience.txt for more details. Bugs fixed: - Fixed a bug that caused extra commit calls to be made on data managers under certain special circumstances. https://mail.zope.org/pipermail/zodb-dev/2010-May/013329.html - When threads were reused, transaction data could leak accross them, causing subtle application bugs. https://bugs.launchpad.net/zodb/+bug/239086 1.0.1 (2010-05-07) ================== - LP #142464: remove double newline between log entries: it makes doing smarter formatting harder. - Updated tests to remove use of deprecated ``zope.testing.doctest``. 1.0.0 (2009-07-24) ================== - Fix test that incorrectly relied on the order of a list that was generated from a dict. - Remove crufty DEPENDENCIES.cfg left over from zpkg. 1.0a1 (2007-12-18) ================== - Initial release, branched from ZODB trunk on 2007-11-08 (aka "3.9.0dev"). - Remove (deprecated) support for beforeCommitHook alias to addBeforeCommitHook. - Add weakset tests. - Remove unit tests that depend on ZODB.tests.utils from test_transaction (these are actually integration tests). ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726644280.0 transaction-5.0/src/transaction.egg-info/SOURCES.txt0000644000076500000240000000324514672500070021264 0ustar00jensstaff.pre-commit-config.yaml .readthedocs.yaml CHANGES.rst CONTRIBUTING.md COPYRIGHT.txt LICENSE.txt MANIFEST.in README.rst pyproject.toml setup.cfg setup.py tox.ini docs/Makefile docs/api.rst docs/changes.rst docs/conf.py docs/convenience.rst docs/datamanager.rst docs/developer.rst docs/doom.rst docs/hooks.rst docs/index.rst docs/integrations.rst docs/make.bat docs/requirements.txt docs/savepoint.rst docs/sqlalchemy.rst docs/_build/doctest/output.txt docs/_build/html/_sources/api.rst.txt docs/_build/html/_sources/changes.rst.txt docs/_build/html/_sources/convenience.rst.txt docs/_build/html/_sources/datamanager.rst.txt docs/_build/html/_sources/developer.rst.txt docs/_build/html/_sources/doom.rst.txt docs/_build/html/_sources/hooks.rst.txt docs/_build/html/_sources/index.rst.txt docs/_build/html/_sources/integrations.rst.txt docs/_build/html/_sources/savepoint.rst.txt docs/_build/html/_sources/sqlalchemy.rst.txt docs/_build/html/_static/placeholder.txt docs/_static/placeholder.txt docs/_templates/placeholder.txt src/transaction/__init__.py src/transaction/_manager.py src/transaction/_transaction.py src/transaction/interfaces.py src/transaction/weakset.py src/transaction.egg-info/PKG-INFO src/transaction.egg-info/SOURCES.txt src/transaction.egg-info/dependency_links.txt src/transaction.egg-info/not-zip-safe src/transaction.egg-info/requires.txt src/transaction.egg-info/top_level.txt src/transaction/tests/__init__.py src/transaction/tests/common.py src/transaction/tests/examples.py src/transaction/tests/savepointsample.py src/transaction/tests/test__manager.py src/transaction/tests/test__transaction.py src/transaction/tests/test_savepoint.py src/transaction/tests/test_weakset.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726644280.0 transaction-5.0/src/transaction.egg-info/dependency_links.txt0000644000076500000240000000000114672500070023442 0ustar00jensstaff ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726577067.0 transaction-5.0/src/transaction.egg-info/not-zip-safe0000644000076500000240000000000114672274653021641 0ustar00jensstaff ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726644280.0 transaction-5.0/src/transaction.egg-info/requires.txt0000644000076500000240000000011614672500070021772 0ustar00jensstaffzope.interface [docs] Sphinx repoze.sphinx.autointerface [testing] coverage ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726644280.0 transaction-5.0/src/transaction.egg-info/top_level.txt0000644000076500000240000000001414672500070022121 0ustar00jensstafftransaction ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726644116.0 transaction-5.0/tox.ini0000644000076500000240000000363314672477624014130 0ustar00jensstaff# Generated from: # https://github.com/zopefoundation/meta/tree/master/config/pure-python [tox] minversion = 3.18 envlist = release-check lint py38 py39 py310 py311 py312 py313 pypy3 docs coverage [testenv] usedevelop = true package = wheel wheel_build_env = .pkg deps = setuptools <74 zope.testrunner setenv = commands = zope-testrunner --test-path=src {posargs:-vc} sphinx-build -b doctest -d {envdir}/.cache/doctrees docs {envdir}/.cache/doctest extras = test docs [testenv:setuptools-latest] basepython = python3 deps = git+https://github.com/pypa/setuptools.git\#egg=setuptools zope.testrunner [testenv:release-check] description = ensure that the distribution is ready to release basepython = python3 skip_install = true deps = setuptools <74 twine build check-manifest check-python-versions >= 0.20.0 wheel commands_pre = commands = check-manifest check-python-versions --only setup.py,tox.ini,.github/workflows/tests.yml python -m build --sdist --no-isolation twine check dist/* [testenv:lint] description = This env runs all linters configured in .pre-commit-config.yaml basepython = python3 skip_install = true deps = pre-commit commands_pre = commands = pre-commit run --all-files --show-diff-on-failure [testenv:docs] basepython = python3 skip_install = false commands_pre = commands = sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html sphinx-build -b doctest -d docs/_build/doctrees docs docs/_build/doctest [testenv:coverage] basepython = python3 allowlist_externals = mkdir deps = coverage[toml] zope.testrunner commands = mkdir -p {toxinidir}/parts/htmlcov coverage run -m zope.testrunner --test-path=src {posargs:-vc} coverage run -a -m sphinx -b doctest -d {envdir}/.cache/doctrees docs {envdir}/.cache/doctest coverage html coverage report