Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Fixed #10771 -- added support for using the transaction management fu…

…nctions 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
  • Loading branch information...
commit 27db9378cfba804f532442d9c3fc48f8249e0d46 1 parent cfbba28
@alex alex authored
View
144 django/db/transaction.py
@@ -11,6 +11,7 @@
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 @@
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,31 +259,79 @@ 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.
+ 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 inner_autocommit(func, db=None):
- def _autocommit(*args, **kw):
+ 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:
- 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)
+ 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):
"""
@@ -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)
+ def entering(using):
+ enter_transaction_management(using=using)
+ managed(True, using=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:
- res = func(*args, **kw)
+ commit(using=using)
except:
- # All exceptions must be handled here (even string ones).
- if is_dirty(using=db):
- rollback(using=db)
+ rollback(using=using)
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)
- # 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)
+ 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)
View
13 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
+<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
~~~~~~~~~~~~~~~
View
173 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
-
- @transaction.commit_manually
- def viewfunc(request):
- ...
- # You can commit/rollback however and whenever you want
- transaction.commit()
- ...
+ from django.db import transaction
- # 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):
- ....
-
-.. 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()``.
+ # But you've got to remember to do it yourself!
+ try:
+ ...
+ except:
+ transaction.rollback()
+ else:
+ transaction.commit()
+
+ @transaction.commit_manually(using="my_other_database")
+ def viewfunc2(request):
+ ....
How to globally deactivate transaction management
=================================================
View
6 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 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)
View
123 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()
Please sign in to comment.
Something went wrong with that request. Please try again.