From 27db9378cfba804f532442d9c3fc48f8249e0d46 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Tue, 19 Oct 2010 19:38:15 +0000 Subject: [PATCH] Fixed #10771 -- added support for using the transaction management functions as context managers in Python 2.5 and above. Thanks to Jacob for help with the docs. git-svn-id: http://code.djangoproject.com/svn/django/trunk@14288 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/db/transaction.py | 154 +++++++++++-------- docs/releases/1.3.txt | 13 ++ docs/topics/db/transactions.txt | 171 +++++++++++++--------- tests/modeltests/transactions/tests.py | 6 + tests/modeltests/transactions/tests_25.py | 123 ++++++++++++++++ 5 files changed, 333 insertions(+), 134 deletions(-) create mode 100644 tests/modeltests/transactions/tests_25.py diff --git a/django/db/transaction.py b/django/db/transaction.py index af42fd5493..7827d04e9b 100644 --- a/django/db/transaction.py +++ b/django/db/transaction.py @@ -11,6 +11,7 @@ called, a commit is made. Managed transactions don't do those commits, but will need some kind of manual or implicit commits or rollbacks. """ +import sys try: import thread @@ -20,8 +21,9 @@ try: from functools import wraps except ImportError: from django.utils.functional import wraps # Python 2.4 fallback. -from django.db import connections, DEFAULT_DB_ALIAS + from django.conf import settings +from django.db import connections, DEFAULT_DB_ALIAS class TransactionManagementError(Exception): """ @@ -257,32 +259,80 @@ def savepoint_commit(sid, using=None): # DECORATORS # ############## -def autocommit(using=None): +class Transaction(object): """ - Decorator that activates commit on save. This is Django's default behavior; - this decorator is useful if you globally activated transaction management in - your settings file and want the default behavior in some view functions. - """ - def inner_autocommit(func, db=None): - def _autocommit(*args, **kw): - try: - enter_transaction_management(managed=False, using=db) - managed(False, using=db) - return func(*args, **kw) - finally: - leave_transaction_management(using=db) - return wraps(func)(_autocommit) + Acts as either a decorator, or a context manager. If it's a decorator it + takes a function and returns a wrapped function. If it's a contextmanager + it's used with the ``with`` statement. In either event entering/exiting + are called before and after, respectively, the function/block is executed. + autocommit, commit_on_success, and commit_manually contain the + implementations of entering and exiting. + """ + def __init__(self, entering, exiting, using): + self.entering = entering + self.exiting = exiting + self.using = using + + def __enter__(self): + self.entering(self.using) + + def __exit__(self, exc_type, exc_value, traceback): + self.exiting(exc_value, self.using) + + def __call__(self, func): + @wraps(func) + def inner(*args, **kwargs): + # Once we drop support for Python 2.4 this block should become: + # with self: + # func(*args, **kwargs) + self.__enter__() + try: + res = func(*args, **kwargs) + except: + self.__exit__(*sys.exc_info()) + raise + else: + self.__exit__(None, None, None) + return res + return inner + +def _transaction_func(entering, exiting, using): + """ + Takes 3 things, an entering function (what to do to start this block of + transaction management), an exiting function (what to do to end it, on both + success and failure, and using which can be: None, indiciating using is + DEFAULT_DB_ALIAS, a callable, indicating that using is DEFAULT_DB_ALIAS and + to return the function already wrapped. + + Returns either a Transaction objects, which is both a decorator and a + context manager, or a wrapped function, if using is a callable. + """ # Note that although the first argument is *called* `using`, it # may actually be a function; @autocommit and @autocommit('foo') # are both allowed forms. if using is None: using = DEFAULT_DB_ALIAS if callable(using): - return inner_autocommit(using, DEFAULT_DB_ALIAS) - return lambda func: inner_autocommit(func, using) + return Transaction(entering, exiting, DEFAULT_DB_ALIAS)(using) + return Transaction(entering, exiting, using) +def autocommit(using=None): + """ + Decorator that activates commit on save. This is Django's default behavior; + this decorator is useful if you globally activated transaction management in + your settings file and want the default behavior in some view functions. + """ + def entering(using): + enter_transaction_management(managed=False, using=using) + managed(False, using=using) + + def exiting(exc_value, using): + leave_transaction_management(using=using) + + return _transaction_func(entering, exiting, using) + def commit_on_success(using=None): """ This decorator activates commit on response. This way, if the view function @@ -290,38 +340,23 @@ def commit_on_success(using=None): a rollback is made. This is one of the most common ways to do transaction control in Web apps. """ - def inner_commit_on_success(func, db=None): - def _commit_on_success(*args, **kw): - try: - enter_transaction_management(using=db) - managed(True, using=db) - try: - res = func(*args, **kw) - except: - # All exceptions must be handled here (even string ones). - if is_dirty(using=db): - rollback(using=db) - raise - else: - if is_dirty(using=db): - try: - commit(using=db) - except: - rollback(using=db) - raise - return res - finally: - leave_transaction_management(using=db) - return wraps(func)(_commit_on_success) + def entering(using): + enter_transaction_management(using=using) + managed(True, using=using) - # Note that although the first argument is *called* `using`, it - # may actually be a function; @autocommit and @autocommit('foo') - # are both allowed forms. - if using is None: - using = DEFAULT_DB_ALIAS - if callable(using): - return inner_commit_on_success(using, DEFAULT_DB_ALIAS) - return lambda func: inner_commit_on_success(func, using) + def exiting(exc_value, using): + if exc_value is not None: + if is_dirty(using=using): + rollback(using=using) + else: + if is_dirty(using=using): + try: + commit(using=using) + except: + rollback(using=using) + raise + + return _transaction_func(entering, exiting, using) def commit_manually(using=None): """ @@ -330,22 +365,11 @@ def commit_manually(using=None): own -- it's up to the user to call the commit and rollback functions themselves. """ - def inner_commit_manually(func, db=None): - def _commit_manually(*args, **kw): - try: - enter_transaction_management(using=db) - managed(True, using=db) - return func(*args, **kw) - finally: - leave_transaction_management(using=db) + def entering(using): + enter_transaction_management(using=using) + managed(True, using=using) - return wraps(func)(_commit_manually) + def exiting(exc_value, using): + leave_transaction_management(using=using) - # Note that although the first argument is *called* `using`, it - # may actually be a function; @autocommit and @autocommit('foo') - # are both allowed forms. - if using is None: - using = DEFAULT_DB_ALIAS - if callable(using): - return inner_commit_manually(using, DEFAULT_DB_ALIAS) - return lambda func: inner_commit_manually(func, using) + return _transaction_func(entering, exiting, using) diff --git a/docs/releases/1.3.txt b/docs/releases/1.3.txt index 8f722f6cbc..39037f423c 100644 --- a/docs/releases/1.3.txt +++ b/docs/releases/1.3.txt @@ -73,6 +73,19 @@ you just won't get any of the nice new unittest2 features. .. _unittest2: http://pypi.python.org/pypi/unittest2 +Transaction context managers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Users of Python 2.5 and above may now use :ref:`transaction management functions +` as `context managers`_. For example:: + + with transaction.autocommit(): + # ... + +.. _context managers: http://docs.python.org/glossary.html#term-context-manager + +For more information, see :ref:`transaction-management-functions`. + Everything else ~~~~~~~~~~~~~~~ diff --git a/docs/topics/db/transactions.txt b/docs/topics/db/transactions.txt index 2d99c17a32..57ac2bfe9d 100644 --- a/docs/topics/db/transactions.txt +++ b/docs/topics/db/transactions.txt @@ -2,7 +2,7 @@ Managing database transactions ============================== -.. currentmodule:: django.db +.. currentmodule:: django.db.transaction Django gives you a few ways to control how database transactions are managed, if you're using a database that supports transactions. @@ -50,105 +50,138 @@ An exception is ``CacheMiddleware``, which is never affected. The cache middleware uses its own database cursor (which is mapped to its own database connection internally). +.. _transaction-management-functions: + Controlling transaction management in views =========================================== -For most people, implicit request-based transactions work wonderfully. However, -if you need more fine-grained control over how transactions are managed, you -can use Python decorators to change the way transactions are handled by a -particular view function. All of the decorators take an option ``using`` -parameter which should be the alias for a database connection for which the -behavior applies to. If no alias is specified then the ``"default"`` database -is used. +.. versionchanged:: 1.3 + Transaction management context managers are new in Django 1.3. -.. note:: +For most people, implicit request-based transactions work wonderfully. However, +if you need more fine-grained control over how transactions are managed, you can +use a set of functions in ``django.db.transaction`` to control transactions on a +per-function or per-code-block basis. + +These functions, described in detail below, can be used in two different ways: + + * As a decorator_ on a particular function. For example:: + + from django.db import transaction + + @transaction.commit_on_success() + def viewfunc(request): + # ... + # this code executes inside a transaction + # ... + + This technique works with all supported version of Python (that is, with + Python 2.4 and greater). + + * As a `context manager`_ around a particular block of code:: + + from django.db import transaction + + def viewfunc(request): + # ... + # this code executes using default transaction management + # ... + + with transaction.commit_on_success(): + # ... + # this code executes inside a transaction + # ... + + The ``with`` statement is new in Python 2.5, and so this syntax can only + be used with Python 2.5 and above. + +.. _decorator: http://docs.python.org/glossary.html#term-decorator +.. _context manager: http://docs.python.org/glossary.html#term-context-manager + +For maximum compatibility, all of the examples below show transactions using the +decorator syntax, but all of the follow functions may be used as context +managers, too. + +.. note:: Although the examples below use view functions as examples, these - decorators can be applied to non-view functions as well. + decorators and context managers can be used anywhere in your code + that you need to deal with transactions. .. _topics-db-transactions-autocommit: -``django.db.transaction.autocommit`` ------------------------------------- +.. function:: autocommit -Use the ``autocommit`` decorator to switch a view function to Django's default -commit behavior, regardless of the global transaction setting. + Use the ``autocommit`` decorator to switch a view function to Django's + default commit behavior, regardless of the global transaction setting. -Example:: + Example:: - from django.db import transaction + from django.db import transaction - @transaction.autocommit - def viewfunc(request): - .... + @transaction.autocommit + def viewfunc(request): + .... - @transaction.autocommit(using="my_other_database") - def viewfunc2(request): - .... + @transaction.autocommit(using="my_other_database") + def viewfunc2(request): + .... -Within ``viewfunc()``, transactions will be committed as soon as you call -``model.save()``, ``model.delete()``, or any other function that writes to the -database. ``viewfunc2()`` will have this same behavior, but for the -``"my_other_database"`` connection. + Within ``viewfunc()``, transactions will be committed as soon as you call + ``model.save()``, ``model.delete()``, or any other function that writes to + the database. ``viewfunc2()`` will have this same behavior, but for the + ``"my_other_database"`` connection. -``django.db.transaction.commit_on_success`` -------------------------------------------- +.. function:: commit_on_success -Use the ``commit_on_success`` decorator to use a single transaction for -all the work done in a function:: + Use the ``commit_on_success`` decorator to use a single transaction for all + the work done in a function:: - from django.db import transaction + from django.db import transaction - @transaction.commit_on_success - def viewfunc(request): - .... + @transaction.commit_on_success + def viewfunc(request): + .... - @transaction.commit_on_success(using="my_other_database") - def viewfunc2(request): - .... + @transaction.commit_on_success(using="my_other_database") + def viewfunc2(request): + .... -If the function returns successfully, then Django will commit all work done -within the function at that point. If the function raises an exception, though, -Django will roll back the transaction. + If the function returns successfully, then Django will commit all work done + within the function at that point. If the function raises an exception, + though, Django will roll back the transaction. -``django.db.transaction.commit_manually`` ------------------------------------------ +.. function:: commit_manually -Use the ``commit_manually`` decorator if you need full control over -transactions. It tells Django you'll be managing the transaction on your own. + Use the ``commit_manually`` decorator if you need full control over + transactions. It tells Django you'll be managing the transaction on your + own. -If your view changes data and doesn't ``commit()`` or ``rollback()``, Django -will raise a ``TransactionManagementError`` exception. + If your view changes data and doesn't ``commit()`` or ``rollback()``, + Django will raise a ``TransactionManagementError`` exception. -Manual transaction management looks like this:: + Manual transaction management looks like this:: - from django.db import transaction + from django.db import transaction - @transaction.commit_manually - def viewfunc(request): - ... - # You can commit/rollback however and whenever you want - transaction.commit() - ... - - # But you've got to remember to do it yourself! - try: + @transaction.commit_manually + def viewfunc(request): ... - except: - transaction.rollback() - else: + # You can commit/rollback however and whenever you want transaction.commit() + ... - @transaction.commit_manually(using="my_other_database") - def viewfunc2(request): - .... + # But you've got to remember to do it yourself! + try: + ... + except: + transaction.rollback() + else: + transaction.commit() -.. admonition:: An important note to users of earlier Django releases: - - The database ``connection.commit()`` and ``connection.rollback()`` methods - (called ``db.commit()`` and ``db.rollback()`` in 0.91 and earlier) no - longer exist. They've been replaced by ``transaction.commit()`` and - ``transaction.rollback()``. + @transaction.commit_manually(using="my_other_database") + def viewfunc2(request): + .... How to globally deactivate transaction management ================================================= diff --git a/tests/modeltests/transactions/tests.py b/tests/modeltests/transactions/tests.py index be95005195..9deb18382c 100644 --- a/tests/modeltests/transactions/tests.py +++ b/tests/modeltests/transactions/tests.py @@ -1,3 +1,5 @@ +import sys + from django.db import connection, transaction, IntegrityError, DEFAULT_DB_ALIAS from django.conf import settings from django.test import TransactionTestCase, skipUnlessDBFeature @@ -5,6 +7,10 @@ from django.test import TransactionTestCase, skipUnlessDBFeature from models import Reporter +if sys.version_info >= (2, 5): + from tests_25 import TransactionContextManagerTests + + class TransactionTests(TransactionTestCase): def create_a_reporter_then_fail(self, first, last): a = Reporter(first_name=first, last_name=last) diff --git a/tests/modeltests/transactions/tests_25.py b/tests/modeltests/transactions/tests_25.py new file mode 100644 index 0000000000..ec3c4d1215 --- /dev/null +++ b/tests/modeltests/transactions/tests_25.py @@ -0,0 +1,123 @@ +from __future__ import with_statement + +from django.db import connection, transaction, IntegrityError +from django.test import TransactionTestCase, skipUnlessDBFeature + +from models import Reporter + + +class TransactionContextManagerTests(TransactionTestCase): + def create_reporter_and_fail(self): + Reporter.objects.create(first_name="Bob", last_name="Holtzman") + raise Exception + + @skipUnlessDBFeature('supports_transactions') + def test_autocommit(self): + """ + The default behavior is to autocommit after each save() action. + """ + with self.assertRaises(Exception): + self.create_reporter_and_fail() + # The object created before the exception still exists + self.assertEqual(Reporter.objects.count(), 1) + + @skipUnlessDBFeature('supports_transactions') + def test_autocommit_context_manager(self): + """ + The autocommit context manager works exactly the same as the default + behavior. + """ + with self.assertRaises(Exception): + with transaction.autocommit(): + self.create_reporter_and_fail() + + self.assertEqual(Reporter.objects.count(), 1) + + @skipUnlessDBFeature('supports_transactions') + def test_autocommit_context_manager_with_using(self): + """ + The autocommit context manager also works with a using argument. + """ + with self.assertRaises(Exception): + with transaction.autocommit(using="default"): + self.create_reporter_and_fail() + + self.assertEqual(Reporter.objects.count(), 1) + + @skipUnlessDBFeature('supports_transactions') + def test_commit_on_success(self): + """ + With the commit_on_success context manager, the transaction is only + committed if the block doesn't throw an exception. + """ + with self.assertRaises(Exception): + with transaction.commit_on_success(): + self.create_reporter_and_fail() + + self.assertEqual(Reporter.objects.count(), 0) + + @skipUnlessDBFeature('supports_transactions') + def test_commit_on_success_with_using(self): + """ + The commit_on_success context manager also works with a using argument. + """ + with self.assertRaises(Exception): + with transaction.commit_on_success(using="default"): + self.create_reporter_and_fail() + + self.assertEqual(Reporter.objects.count(), 0) + + @skipUnlessDBFeature('supports_transactions') + def test_commit_on_success_succeed(self): + """ + If there aren't any exceptions, the data will get saved. + """ + Reporter.objects.create(first_name="Alice", last_name="Smith") + with transaction.commit_on_success(): + Reporter.objects.filter(first_name="Alice").delete() + + self.assertQuerysetEqual(Reporter.objects.all(), []) + + @skipUnlessDBFeature('supports_transactions') + def test_manually_managed(self): + """ + You can manually manage transactions if you really want to, but you + have to remember to commit/rollback. + """ + with transaction.commit_manually(): + Reporter.objects.create(first_name="Libby", last_name="Holtzman") + transaction.commit() + self.assertEqual(Reporter.objects.count(), 1) + + @skipUnlessDBFeature('supports_transactions') + def test_manually_managed_mistake(self): + """ + If you forget, you'll get bad errors. + """ + with self.assertRaises(transaction.TransactionManagementError): + with transaction.commit_manually(): + Reporter.objects.create(first_name="Scott", last_name="Browning") + + @skipUnlessDBFeature('supports_transactions') + def test_manually_managed_with_using(self): + """ + The commit_manually function also works with a using argument. + """ + with self.assertRaises(transaction.TransactionManagementError): + with transaction.commit_manually(using="default"): + Reporter.objects.create(first_name="Walter", last_name="Cronkite") + + @skipUnlessDBFeature('requires_rollback_on_dirty_transaction') + def test_bad_sql(self): + """ + Regression for #11900: If a block wrapped by commit_on_success + writes a transaction that can't be committed, that transaction should + be rolled back. The bug is only visible using the psycopg2 backend, + though the fix is generally a good idea. + """ + with self.assertRaises(IntegrityError): + with transaction.commit_on_success(): + cursor = connection.cursor() + cursor.execute("INSERT INTO transactions_reporter (first_name, last_name) VALUES ('Douglas', 'Adams');") + transaction.set_dirty() + transaction.rollback()