Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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 Gaynor authored October 19, 2010
144  django/db/transaction.py
@@ -11,6 +11,7 @@
11 11
 Managed transactions don't do those commits, but will need some kind of manual
12 12
 or implicit commits or rollbacks.
13 13
 """
  14
+import sys
14 15
 
15 16
 try:
16 17
     import thread
@@ -20,8 +21,9 @@
20 21
     from functools import wraps
21 22
 except ImportError:
22 23
     from django.utils.functional import wraps  # Python 2.4 fallback.
23  
-from django.db import connections, DEFAULT_DB_ALIAS
  24
+
24 25
 from django.conf import settings
  26
+from django.db import connections, DEFAULT_DB_ALIAS
25 27
 
26 28
 class TransactionManagementError(Exception):
27 29
     """
@@ -257,31 +259,79 @@ def savepoint_commit(sid, using=None):
257 259
 # DECORATORS #
258 260
 ##############
259 261
 
260  
-def autocommit(using=None):
  262
+class Transaction(object):
261 263
     """
262  
-    Decorator that activates commit on save. This is Django's default behavior;
263  
-    this decorator is useful if you globally activated transaction management in
264  
-    your settings file and want the default behavior in some view functions.
  264
+    Acts as either a decorator, or a context manager.  If it's a decorator it
  265
+    takes a function and returns a wrapped function.  If it's a contextmanager
  266
+    it's used with the ``with`` statement.  In either event entering/exiting
  267
+    are called before and after, respectively, the function/block is executed.
  268
+
  269
+    autocommit, commit_on_success, and commit_manually contain the
  270
+    implementations of entering and exiting.
265 271
     """
266  
-    def inner_autocommit(func, db=None):
267  
-        def _autocommit(*args, **kw):
  272
+    def __init__(self, entering, exiting, using):
  273
+        self.entering = entering
  274
+        self.exiting = exiting
  275
+        self.using = using
  276
+
  277
+    def __enter__(self):
  278
+        self.entering(self.using)
  279
+
  280
+    def __exit__(self, exc_type, exc_value, traceback):
  281
+        self.exiting(exc_value, self.using)
  282
+
  283
+    def __call__(self, func):
  284
+        @wraps(func)
  285
+        def inner(*args, **kwargs):
  286
+            # Once we drop support for Python 2.4 this block should become:
  287
+            # with self:
  288
+            #     func(*args, **kwargs)
  289
+            self.__enter__()
268 290
             try:
269  
-                enter_transaction_management(managed=False, using=db)
270  
-                managed(False, using=db)
271  
-                return func(*args, **kw)
272  
-            finally:
273  
-                leave_transaction_management(using=db)
274  
-        return wraps(func)(_autocommit)
  291
+                res = func(*args, **kwargs)
  292
+            except:
  293
+                self.__exit__(*sys.exc_info())
  294
+                raise
  295
+            else:
  296
+                self.__exit__(None, None, None)
  297
+                return res
  298
+        return inner
275 299
 
  300
+def _transaction_func(entering, exiting, using):
  301
+    """
  302
+    Takes 3 things, an entering function (what to do to start this block of
  303
+    transaction management), an exiting function (what to do to end it, on both
  304
+    success and failure, and using which can be: None, indiciating using is
  305
+    DEFAULT_DB_ALIAS, a callable, indicating that using is DEFAULT_DB_ALIAS and
  306
+    to return the function already wrapped.
  307
+
  308
+    Returns either a Transaction objects, which is both a decorator and a
  309
+    context manager, or a wrapped function, if using is a callable.
  310
+    """
276 311
     # Note that although the first argument is *called* `using`, it
277 312
     # may actually be a function; @autocommit and @autocommit('foo')
278 313
     # are both allowed forms.
279 314
     if using is None:
280 315
         using = DEFAULT_DB_ALIAS
281 316
     if callable(using):
282  
-        return inner_autocommit(using, DEFAULT_DB_ALIAS)
283  
-    return lambda func: inner_autocommit(func,  using)
  317
+        return Transaction(entering, exiting, DEFAULT_DB_ALIAS)(using)
  318
+    return Transaction(entering, exiting, using)
  319
+
  320
+
  321
+def autocommit(using=None):
  322
+    """
  323
+    Decorator that activates commit on save. This is Django's default behavior;
  324
+    this decorator is useful if you globally activated transaction management in
  325
+    your settings file and want the default behavior in some view functions.
  326
+    """
  327
+    def entering(using):
  328
+        enter_transaction_management(managed=False, using=using)
  329
+        managed(False, using=using)
284 330
 
  331
+    def exiting(exc_value, using):
  332
+        leave_transaction_management(using=using)
  333
+
  334
+    return _transaction_func(entering, exiting, using)
285 335
 
286 336
 def commit_on_success(using=None):
287 337
     """
@@ -290,38 +340,23 @@ def commit_on_success(using=None):
290 340
     a rollback is made. This is one of the most common ways to do transaction
291 341
     control in Web apps.
292 342
     """
293  
-    def inner_commit_on_success(func, db=None):
294  
-        def _commit_on_success(*args, **kw):
295  
-            try:
296  
-                enter_transaction_management(using=db)
297  
-                managed(True, using=db)
  343
+    def entering(using):
  344
+        enter_transaction_management(using=using)
  345
+        managed(True, using=using)
  346
+
  347
+    def exiting(exc_value, using):
  348
+        if exc_value is not None:
  349
+            if is_dirty(using=using):
  350
+                rollback(using=using)
  351
+        else:
  352
+            if is_dirty(using=using):
298 353
                 try:
299  
-                    res = func(*args, **kw)
  354
+                    commit(using=using)
300 355
                 except:
301  
-                    # All exceptions must be handled here (even string ones).
302  
-                    if is_dirty(using=db):
303  
-                        rollback(using=db)
  356
+                    rollback(using=using)
304 357
                     raise
305  
-                else:
306  
-                    if is_dirty(using=db):
307  
-                        try:
308  
-                            commit(using=db)
309  
-                        except:
310  
-                            rollback(using=db)
311  
-                            raise
312  
-                return res
313  
-            finally:
314  
-                leave_transaction_management(using=db)
315  
-        return wraps(func)(_commit_on_success)
316 358
 
317  
-    # Note that although the first argument is *called* `using`, it
318  
-    # may actually be a function; @autocommit and @autocommit('foo')
319  
-    # are both allowed forms.
320  
-    if using is None:
321  
-        using = DEFAULT_DB_ALIAS
322  
-    if callable(using):
323  
-        return inner_commit_on_success(using, DEFAULT_DB_ALIAS)
324  
-    return lambda func: inner_commit_on_success(func, using)
  359
+    return _transaction_func(entering, exiting, using)
325 360
 
326 361
 def commit_manually(using=None):
327 362
     """
@@ -330,22 +365,11 @@ def commit_manually(using=None):
330 365
     own -- it's up to the user to call the commit and rollback functions
331 366
     themselves.
332 367
     """
333  
-    def inner_commit_manually(func, db=None):
334  
-        def _commit_manually(*args, **kw):
335  
-            try:
336  
-                enter_transaction_management(using=db)
337  
-                managed(True, using=db)
338  
-                return func(*args, **kw)
339  
-            finally:
340  
-                leave_transaction_management(using=db)
  368
+    def entering(using):
  369
+        enter_transaction_management(using=using)
  370
+        managed(True, using=using)
341 371
 
342  
-        return wraps(func)(_commit_manually)
  372
+    def exiting(exc_value, using):
  373
+        leave_transaction_management(using=using)
343 374
 
344  
-    # Note that although the first argument is *called* `using`, it
345  
-    # may actually be a function; @autocommit and @autocommit('foo')
346  
-    # are both allowed forms.
347  
-    if using is None:
348  
-        using = DEFAULT_DB_ALIAS
349  
-    if callable(using):
350  
-        return inner_commit_manually(using, DEFAULT_DB_ALIAS)
351  
-    return lambda func: inner_commit_manually(func, using)
  375
+    return _transaction_func(entering, exiting, using)
13  docs/releases/1.3.txt
@@ -73,6 +73,19 @@ you just won't get any of the nice new unittest2 features.
73 73
 
74 74
 .. _unittest2: http://pypi.python.org/pypi/unittest2
75 75
 
  76
+Transaction context managers
  77
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  78
+
  79
+Users of Python 2.5 and above may now use :ref:`transaction management functions
  80
+<transaction-management-functions>` as `context managers`_. For example::
  81
+
  82
+    with transaction.autocommit():
  83
+        # ...
  84
+
  85
+.. _context managers: http://docs.python.org/glossary.html#term-context-manager
  86
+
  87
+For more information, see :ref:`transaction-management-functions`.
  88
+
76 89
 Everything else
77 90
 ~~~~~~~~~~~~~~~
78 91
 
173  docs/topics/db/transactions.txt
@@ -2,7 +2,7 @@
2 2
 Managing database transactions
3 3
 ==============================
4 4
 
5  
-.. currentmodule:: django.db
  5
+.. currentmodule:: django.db.transaction
6 6
 
7 7
 Django gives you a few ways to control how database transactions are managed,
8 8
 if you're using a database that supports transactions.
@@ -50,105 +50,138 @@ An exception is ``CacheMiddleware``, which is never affected. The cache
50 50
 middleware uses its own database cursor (which is mapped to its own database
51 51
 connection internally).
52 52
 
  53
+.. _transaction-management-functions:
  54
+
53 55
 Controlling transaction management in views
54 56
 ===========================================
55 57
 
56  
-For most people, implicit request-based transactions work wonderfully. However,
57  
-if you need more fine-grained control over how transactions are managed, you
58  
-can use Python decorators to change the way transactions are handled by a
59  
-particular view function.  All of the decorators take an option ``using``
60  
-parameter which should be the alias for a database connection for which the
61  
-behavior applies to.  If no alias is specified then the ``"default"`` database
62  
-is used.
  58
+.. versionchanged:: 1.3
  59
+   Transaction management context managers are new in Django 1.3.
63 60
 
64  
-.. note::
  61
+For most people, implicit request-based transactions work wonderfully. However,
  62
+if you need more fine-grained control over how transactions are managed, you can
  63
+use a set of functions in ``django.db.transaction`` to control transactions on a
  64
+per-function or per-code-block basis.
  65
+
  66
+These functions, described in detail below, can be used in two different ways:
  67
+
  68
+    * As a decorator_ on a particular function. For example::
  69
+    
  70
+            from django.db import transaction
  71
+            
  72
+            @transaction.commit_on_success()
  73
+            def viewfunc(request):
  74
+                # ...
  75
+                # this code executes inside a transaction
  76
+                # ...
  77
+                
  78
+      This technique works with all supported version of Python (that is, with
  79
+      Python 2.4 and greater).
  80
+      
  81
+    * As a `context manager`_ around a particular block of code::
  82
+    
  83
+            from django.db import transaction
  84
+            
  85
+            def viewfunc(request):
  86
+                # ...
  87
+                # this code executes using default transaction management
  88
+                # ... 
  89
+                
  90
+                with transaction.commit_on_success():
  91
+                    # ...
  92
+                    # this code executes inside a transaction
  93
+                    # ...
  94
+                    
  95
+      The ``with`` statement is new in Python 2.5, and so this syntax can only
  96
+      be used with Python 2.5 and above.
  97
+      
  98
+.. _decorator: http://docs.python.org/glossary.html#term-decorator
  99
+.. _context manager: http://docs.python.org/glossary.html#term-context-manager
  100
+
  101
+For maximum compatibility, all of the examples below show transactions using the
  102
+decorator syntax, but all of the follow functions may be used as context
  103
+managers, too.
  104
+
  105
+.. note:: 
65 106
 
66 107
     Although the examples below use view functions as examples, these
67  
-    decorators can be applied to non-view functions as well.
  108
+    decorators and context managers can be used anywhere in your code
  109
+    that you need to deal with transactions.
68 110
 
69 111
 .. _topics-db-transactions-autocommit:
70 112
 
71  
-``django.db.transaction.autocommit``
72  
-------------------------------------
  113
+.. function:: autocommit
73 114
 
74  
-Use the ``autocommit`` decorator to switch a view function to Django's default
75  
-commit behavior, regardless of the global transaction setting.
  115
+    Use the ``autocommit`` decorator to switch a view function to Django's
  116
+    default commit behavior, regardless of the global transaction setting.
76 117
 
77  
-Example::
  118
+    Example::
78 119
 
79  
-    from django.db import transaction
  120
+        from django.db import transaction
80 121
 
81  
-    @transaction.autocommit
82  
-    def viewfunc(request):
83  
-        ....
  122
+        @transaction.autocommit
  123
+        def viewfunc(request):
  124
+            ....
84 125
 
85  
-    @transaction.autocommit(using="my_other_database")
86  
-    def viewfunc2(request):
87  
-        ....
  126
+        @transaction.autocommit(using="my_other_database")
  127
+        def viewfunc2(request):
  128
+            ....
88 129
 
89  
-Within ``viewfunc()``, transactions will be committed as soon as you call
90  
-``model.save()``, ``model.delete()``, or any other function that writes to the
91  
-database.  ``viewfunc2()`` will have this same behavior, but for the
92  
-``"my_other_database"`` connection.
  130
+    Within ``viewfunc()``, transactions will be committed as soon as you call
  131
+    ``model.save()``, ``model.delete()``, or any other function that writes to
  132
+    the database.  ``viewfunc2()`` will have this same behavior, but for the
  133
+    ``"my_other_database"`` connection.
93 134
 
94  
-``django.db.transaction.commit_on_success``
95  
--------------------------------------------
  135
+.. function:: commit_on_success
96 136
 
97  
-Use the ``commit_on_success`` decorator to use a single transaction for
98  
-all the work done in a function::
  137
+    Use the ``commit_on_success`` decorator to use a single transaction for all
  138
+    the work done in a function::
99 139
 
100  
-    from django.db import transaction
  140
+        from django.db import transaction
101 141
 
102  
-    @transaction.commit_on_success
103  
-    def viewfunc(request):
104  
-        ....
  142
+        @transaction.commit_on_success
  143
+        def viewfunc(request):
  144
+            ....
105 145
 
106  
-    @transaction.commit_on_success(using="my_other_database")
107  
-    def viewfunc2(request):
108  
-        ....
  146
+        @transaction.commit_on_success(using="my_other_database")
  147
+        def viewfunc2(request):
  148
+            ....
109 149
 
110  
-If the function returns successfully, then Django will commit all work done
111  
-within the function at that point. If the function raises an exception, though,
112  
-Django will roll back the transaction.
  150
+    If the function returns successfully, then Django will commit all work done
  151
+    within the function at that point. If the function raises an exception,
  152
+    though, Django will roll back the transaction.
113 153
 
114  
-``django.db.transaction.commit_manually``
115  
------------------------------------------
  154
+.. function:: commit_manually
116 155
 
117  
-Use the ``commit_manually`` decorator if you need full control over
118  
-transactions. It tells Django you'll be managing the transaction on your own.
  156
+    Use the ``commit_manually`` decorator if you need full control over
  157
+    transactions. It tells Django you'll be managing the transaction on your
  158
+    own.
119 159
 
120  
-If your view changes data and doesn't ``commit()`` or ``rollback()``, Django
121  
-will raise a ``TransactionManagementError`` exception.
  160
+    If your view changes data and doesn't ``commit()`` or ``rollback()``,
  161
+    Django will raise a ``TransactionManagementError`` exception.
122 162
 
123  
-Manual transaction management looks like this::
  163
+    Manual transaction management looks like this::
124 164
 
125  
-    from django.db import transaction
126  
-
127  
-    @transaction.commit_manually
128  
-    def viewfunc(request):
129  
-        ...
130  
-        # You can commit/rollback however and whenever you want
131  
-        transaction.commit()
132  
-        ...
  165
+        from django.db import transaction
133 166
 
134  
-        # But you've got to remember to do it yourself!
135  
-        try:
  167
+        @transaction.commit_manually
  168
+        def viewfunc(request):
136 169
             ...
137  
-        except:
138  
-            transaction.rollback()
139  
-        else:
  170
+            # You can commit/rollback however and whenever you want
140 171
             transaction.commit()
  172
+            ...
141 173
 
142  
-    @transaction.commit_manually(using="my_other_database")
143  
-    def viewfunc2(request):
144  
-        ....
145  
-
146  
-.. admonition:: An important note to users of earlier Django releases:
147  
-
148  
-    The database ``connection.commit()`` and ``connection.rollback()`` methods
149  
-    (called ``db.commit()`` and ``db.rollback()`` in 0.91 and earlier) no
150  
-    longer exist. They've been replaced by ``transaction.commit()`` and
151  
-    ``transaction.rollback()``.
  174
+            # But you've got to remember to do it yourself!
  175
+            try:
  176
+                ...
  177
+            except:
  178
+                transaction.rollback()
  179
+            else:
  180
+                transaction.commit()
  181
+
  182
+        @transaction.commit_manually(using="my_other_database")
  183
+        def viewfunc2(request):
  184
+            ....
152 185
 
153 186
 How to globally deactivate transaction management
154 187
 =================================================
6  tests/modeltests/transactions/tests.py
... ...
@@ -1,3 +1,5 @@
  1
+import sys
  2
+
1 3
 from django.db import connection, transaction, IntegrityError, DEFAULT_DB_ALIAS
2 4
 from django.conf import settings
3 5
 from django.test import TransactionTestCase, skipUnlessDBFeature
@@ -5,6 +7,10 @@
5 7
 from models import Reporter
6 8
 
7 9
 
  10
+if sys.version_info >= (2, 5):
  11
+    from tests_25 import TransactionContextManagerTests
  12
+
  13
+
8 14
 class TransactionTests(TransactionTestCase):
9 15
     def create_a_reporter_then_fail(self, first, last):
10 16
         a = Reporter(first_name=first, last_name=last)
123  tests/modeltests/transactions/tests_25.py
... ...
@@ -0,0 +1,123 @@
  1
+from __future__ import with_statement
  2
+
  3
+from django.db import connection, transaction, IntegrityError
  4
+from django.test import TransactionTestCase, skipUnlessDBFeature
  5
+
  6
+from models import Reporter
  7
+
  8
+
  9
+class TransactionContextManagerTests(TransactionTestCase):
  10
+    def create_reporter_and_fail(self):
  11
+        Reporter.objects.create(first_name="Bob", last_name="Holtzman")
  12
+        raise Exception
  13
+
  14
+    @skipUnlessDBFeature('supports_transactions')
  15
+    def test_autocommit(self):
  16
+        """
  17
+        The default behavior is to autocommit after each save() action.
  18
+        """
  19
+        with self.assertRaises(Exception):
  20
+            self.create_reporter_and_fail()
  21
+        # The object created before the exception still exists
  22
+        self.assertEqual(Reporter.objects.count(), 1)
  23
+
  24
+    @skipUnlessDBFeature('supports_transactions')
  25
+    def test_autocommit_context_manager(self):
  26
+        """
  27
+        The autocommit context manager works exactly the same as the default
  28
+        behavior.
  29
+        """
  30
+        with self.assertRaises(Exception):
  31
+            with transaction.autocommit():
  32
+                self.create_reporter_and_fail()
  33
+
  34
+        self.assertEqual(Reporter.objects.count(), 1)
  35
+
  36
+    @skipUnlessDBFeature('supports_transactions')
  37
+    def test_autocommit_context_manager_with_using(self):
  38
+        """
  39
+        The autocommit context manager also works with a using argument.
  40
+        """
  41
+        with self.assertRaises(Exception):
  42
+            with transaction.autocommit(using="default"):
  43
+                self.create_reporter_and_fail()
  44
+
  45
+        self.assertEqual(Reporter.objects.count(), 1)
  46
+
  47
+    @skipUnlessDBFeature('supports_transactions')
  48
+    def test_commit_on_success(self):
  49
+        """
  50
+        With the commit_on_success context manager, the transaction is only
  51
+        committed if the block doesn't throw an exception.
  52
+        """
  53
+        with self.assertRaises(Exception):
  54
+            with transaction.commit_on_success():
  55
+                self.create_reporter_and_fail()
  56
+
  57
+        self.assertEqual(Reporter.objects.count(), 0)
  58
+
  59
+    @skipUnlessDBFeature('supports_transactions')
  60
+    def test_commit_on_success_with_using(self):
  61
+        """
  62
+        The commit_on_success context manager also works with a using argument.
  63
+        """
  64
+        with self.assertRaises(Exception):
  65
+            with transaction.commit_on_success(using="default"):
  66
+                self.create_reporter_and_fail()
  67
+
  68
+        self.assertEqual(Reporter.objects.count(), 0)
  69
+
  70
+    @skipUnlessDBFeature('supports_transactions')
  71
+    def test_commit_on_success_succeed(self):
  72
+        """
  73
+        If there aren't any exceptions, the data will get saved.
  74
+        """
  75
+        Reporter.objects.create(first_name="Alice", last_name="Smith")
  76
+        with transaction.commit_on_success():
  77
+            Reporter.objects.filter(first_name="Alice").delete()
  78
+
  79
+        self.assertQuerysetEqual(Reporter.objects.all(), [])
  80
+
  81
+    @skipUnlessDBFeature('supports_transactions')
  82
+    def test_manually_managed(self):
  83
+        """
  84
+        You can manually manage transactions if you really want to, but you
  85
+        have to remember to commit/rollback.
  86
+        """
  87
+        with transaction.commit_manually():
  88
+            Reporter.objects.create(first_name="Libby", last_name="Holtzman")
  89
+            transaction.commit()
  90
+        self.assertEqual(Reporter.objects.count(), 1)
  91
+
  92
+    @skipUnlessDBFeature('supports_transactions')
  93
+    def test_manually_managed_mistake(self):
  94
+        """
  95
+        If you forget, you'll get bad errors.
  96
+        """
  97
+        with self.assertRaises(transaction.TransactionManagementError):
  98
+            with transaction.commit_manually():
  99
+                Reporter.objects.create(first_name="Scott", last_name="Browning")
  100
+
  101
+    @skipUnlessDBFeature('supports_transactions')
  102
+    def test_manually_managed_with_using(self):
  103
+        """
  104
+        The commit_manually function also works with a using argument.
  105
+        """
  106
+        with self.assertRaises(transaction.TransactionManagementError):
  107
+            with transaction.commit_manually(using="default"):
  108
+                Reporter.objects.create(first_name="Walter", last_name="Cronkite")
  109
+
  110
+    @skipUnlessDBFeature('requires_rollback_on_dirty_transaction')
  111
+    def test_bad_sql(self):
  112
+        """
  113
+        Regression for #11900: If a block wrapped by commit_on_success
  114
+        writes a transaction that can't be committed, that transaction should
  115
+        be rolled back. The bug is only visible using the psycopg2 backend,
  116
+        though the fix is generally a good idea.
  117
+        """
  118
+        with self.assertRaises(IntegrityError):
  119
+            with transaction.commit_on_success():
  120
+                cursor = connection.cursor()
  121
+                cursor.execute("INSERT INTO transactions_reporter (first_name, last_name) VALUES ('Douglas', 'Adams');")
  122
+                transaction.set_dirty()
  123
+        transaction.rollback()

0 notes on commit 27db937

Please sign in to comment.
Something went wrong with that request. Please try again.