Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Implemented an 'atomic' decorator and context manager.

Currently it only works in autocommit mode.

Based on @xact by Christophe Pettus.
  • Loading branch information...
commit d7bc4fbc94df6c231d71dffa45cf337ff13512ee 1 parent 4b31a6a
Aymeric Augustin authored
1  AUTHORS
@@ -434,6 +434,7 @@ answer newbie questions, and generally made Django that much better:
434 434
     Andreas Pelme <andreas@pelme.se>
435 435
     permonik@mesias.brnonet.cz
436 436
     peter@mymart.com
  437
+    Christophe Pettus <xof@thebuild.com>
437 438
     pgross@thoughtworks.com
438 439
     phaedo <http://phaedo.cx/>
439 440
     phil@produxion.net
23  django/db/backends/__init__.py
@@ -50,6 +50,12 @@ def __init__(self, settings_dict, alias=DEFAULT_DB_ALIAS,
50 50
         # set somewhat aggressively, as the DBAPI doesn't make it easy to
51 51
         # deduce if the connection is in transaction or not.
52 52
         self._dirty = False
  53
+        # Tracks if the connection is in a transaction managed by 'atomic'
  54
+        self.in_atomic_block = False
  55
+        # List of savepoints created by 'atomic'
  56
+        self.savepoint_ids = []
  57
+        # Hack to provide compatibility with legacy transaction management
  58
+        self._atomic_forced_unmanaged = False
53 59
 
54 60
         # Connection termination related attributes
55 61
         self.close_at = None
@@ -148,7 +154,7 @@ def cursor(self):
148 154
 
149 155
     def commit(self):
150 156
         """
151  
-        Does the commit itself and resets the dirty flag.
  157
+        Commits a transaction and resets the dirty flag.
152 158
         """
153 159
         self.validate_thread_sharing()
154 160
         self._commit()
@@ -156,7 +162,7 @@ def commit(self):
156 162
 
157 163
     def rollback(self):
158 164
         """
159  
-        Does the rollback itself and resets the dirty flag.
  165
+        Rolls back a transaction and resets the dirty flag.
160 166
         """
161 167
         self.validate_thread_sharing()
162 168
         self._rollback()
@@ -447,6 +453,12 @@ def temporary_connection(self):
447 453
             if must_close:
448 454
                 self.close()
449 455
 
  456
+    def _start_transaction_under_autocommit(self):
  457
+        """
  458
+        Only required when autocommits_when_autocommit_is_off = True.
  459
+        """
  460
+        raise NotImplementedError
  461
+
450 462
 
451 463
 class BaseDatabaseFeatures(object):
452 464
     allows_group_by_pk = False
@@ -549,6 +561,10 @@ class BaseDatabaseFeatures(object):
549 561
     # Support for the DISTINCT ON clause
550 562
     can_distinct_on_fields = False
551 563
 
  564
+    # Does the backend decide to commit before SAVEPOINT statements
  565
+    # when autocommit is disabled? http://bugs.python.org/issue8145#msg109965
  566
+    autocommits_when_autocommit_is_off = False
  567
+
552 568
     def __init__(self, connection):
553 569
         self.connection = connection
554 570
 
@@ -931,6 +947,9 @@ def start_transaction_sql(self):
931 947
         return "BEGIN;"
932 948
 
933 949
     def end_transaction_sql(self, success=True):
  950
+        """
  951
+        Returns the SQL statement required to end a transaction.
  952
+        """
934 953
         if not success:
935 954
             return "ROLLBACK;"
936 955
         return "COMMIT;"
19  django/db/backends/sqlite3/base.py
@@ -99,6 +99,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
99 99
     supports_mixed_date_datetime_comparisons = False
100 100
     has_bulk_insert = True
101 101
     can_combine_inserts_with_and_without_auto_increment_pk = False
  102
+    autocommits_when_autocommit_is_off = True
102 103
 
103 104
     @cached_property
104 105
     def uses_savepoints(self):
@@ -360,10 +361,12 @@ def close(self):
360 361
             BaseDatabaseWrapper.close(self)
361 362
 
362 363
     def _savepoint_allowed(self):
363  
-        # When 'isolation_level' is None, Django doesn't provide a way to
364  
-        # create a transaction (yet) so savepoints can't be created. When it
365  
-        # isn't, sqlite3 commits before each savepoint -- it's a bug.
366  
-        return False
  364
+        # When 'isolation_level' is not None, sqlite3 commits before each
  365
+        # savepoint; it's a bug. When it is None, savepoints don't make sense
  366
+        # because autocommit is enabled. The only exception is inside atomic
  367
+        # blocks. To work around that bug, on SQLite, atomic starts a
  368
+        # transaction explicitly rather than simply disable autocommit.
  369
+        return self.in_atomic_block
367 370
 
368 371
     def _set_autocommit(self, autocommit):
369 372
         if autocommit:
@@ -413,6 +416,14 @@ def check_constraints(self, table_names=None):
413 416
     def is_usable(self):
414 417
         return True
415 418
 
  419
+    def _start_transaction_under_autocommit(self):
  420
+        """
  421
+        Start a transaction explicitly in autocommit mode.
  422
+
  423
+        Staying in autocommit mode works around a bug of sqlite3 that breaks
  424
+        savepoints when autocommit is disabled.
  425
+        """
  426
+        self.cursor().execute("BEGIN")
416 427
 
417 428
 FORMAT_QMARK_REGEX = re.compile(r'(?<!%)%s')
418 429
 
157  django/db/transaction.py
@@ -16,7 +16,7 @@
16 16
 
17 17
 from functools import wraps
18 18
 
19  
-from django.db import connections, DEFAULT_DB_ALIAS
  19
+from django.db import connections, DatabaseError, DEFAULT_DB_ALIAS
20 20
 
21 21
 
22 22
 class TransactionManagementError(Exception):
@@ -134,13 +134,13 @@ def rollback_unless_managed(using=None):
134 134
 
135 135
 def commit(using=None):
136 136
     """
137  
-    Does the commit itself and resets the dirty flag.
  137
+    Commits a transaction and resets the dirty flag.
138 138
     """
139 139
     get_connection(using).commit()
140 140
 
141 141
 def rollback(using=None):
142 142
     """
143  
-    This function does the rollback itself and resets the dirty flag.
  143
+    Rolls back a transaction and resets the dirty flag.
144 144
     """
145 145
     get_connection(using).rollback()
146 146
 
@@ -166,9 +166,151 @@ def savepoint_commit(sid, using=None):
166 166
     """
167 167
     get_connection(using).savepoint_commit(sid)
168 168
 
169  
-##############
170  
-# DECORATORS #
171  
-##############
  169
+
  170
+#################################
  171
+# Decorators / context managers #
  172
+#################################
  173
+
  174
+class Atomic(object):
  175
+    """
  176
+    This class guarantees the atomic execution of a given block.
  177
+
  178
+    An instance can be used either as a decorator or as a context manager.
  179
+
  180
+    When it's used as a decorator, __call__ wraps the execution of the
  181
+    decorated function in the instance itself, used as a context manager.
  182
+
  183
+    When it's used as a context manager, __enter__ creates a transaction or a
  184
+    savepoint, depending on whether a transaction is already in progress, and
  185
+    __exit__ commits the transaction or releases the savepoint on normal exit,
  186
+    and rolls back the transaction or to the savepoint on exceptions.
  187
+
  188
+    A stack of savepoints identifiers is maintained as an attribute of the
  189
+    connection. None denotes a plain transaction.
  190
+
  191
+    This allows reentrancy even if the same AtomicWrapper is reused. For
  192
+    example, it's possible to define `oa = @atomic('other')` and use `@ao` or
  193
+    `with oa:` multiple times.
  194
+
  195
+    Since database connections are thread-local, this is thread-safe.
  196
+    """
  197
+
  198
+    def __init__(self, using):
  199
+        self.using = using
  200
+
  201
+    def _legacy_enter_transaction_management(self, connection):
  202
+        if not connection.in_atomic_block:
  203
+            if connection.transaction_state and connection.transaction_state[-1]:
  204
+                connection._atomic_forced_unmanaged = True
  205
+                connection.enter_transaction_management(managed=False)
  206
+            else:
  207
+                connection._atomic_forced_unmanaged = False
  208
+
  209
+    def _legacy_leave_transaction_management(self, connection):
  210
+        if not connection.in_atomic_block and connection._atomic_forced_unmanaged:
  211
+            connection.leave_transaction_management()
  212
+
  213
+    def __enter__(self):
  214
+        connection = get_connection(self.using)
  215
+
  216
+        # Ensure we have a connection to the database before testing
  217
+        # autocommit status.
  218
+        connection.ensure_connection()
  219
+
  220
+        # Remove this when the legacy transaction management goes away.
  221
+        self._legacy_enter_transaction_management(connection)
  222
+
  223
+        if not connection.in_atomic_block and not connection.autocommit:
  224
+            raise TransactionManagementError(
  225
+                "'atomic' cannot be used when autocommit is disabled.")
  226
+
  227
+        if connection.in_atomic_block:
  228
+            # We're already in a transaction; create a savepoint.
  229
+            sid = connection.savepoint()
  230
+            connection.savepoint_ids.append(sid)
  231
+        else:
  232
+            # We aren't in a transaction yet; create one.
  233
+            # The usual way to start a transaction is to turn autocommit off.
  234
+            # However, some database adapters (namely sqlite3) don't handle
  235
+            # transactions and savepoints properly when autocommit is off.
  236
+            # In such cases, start an explicit transaction instead, which has
  237
+            # the side-effect of disabling autocommit.
  238
+            if connection.features.autocommits_when_autocommit_is_off:
  239
+                connection._start_transaction_under_autocommit()
  240
+                connection.autocommit = False
  241
+            else:
  242
+                connection.set_autocommit(False)
  243
+            connection.in_atomic_block = True
  244
+            connection.savepoint_ids.append(None)
  245
+
  246
+    def __exit__(self, exc_type, exc_value, traceback):
  247
+        connection = get_connection(self.using)
  248
+        sid = connection.savepoint_ids.pop()
  249
+        if exc_value is None:
  250
+            if sid is None:
  251
+                # Commit transaction
  252
+                connection.in_atomic_block = False
  253
+                try:
  254
+                    connection.commit()
  255
+                except DatabaseError:
  256
+                    connection.rollback()
  257
+                    # Remove this when the legacy transaction management goes away.
  258
+                    self._legacy_leave_transaction_management(connection)
  259
+                    raise
  260
+                finally:
  261
+                    if connection.features.autocommits_when_autocommit_is_off:
  262
+                        connection.autocommit = True
  263
+                    else:
  264
+                        connection.set_autocommit(True)
  265
+            else:
  266
+                # Release savepoint
  267
+                try:
  268
+                    connection.savepoint_commit(sid)
  269
+                except DatabaseError:
  270
+                    connection.savepoint_rollback(sid)
  271
+                    # Remove this when the legacy transaction management goes away.
  272
+                    self._legacy_leave_transaction_management(connection)
  273
+                    raise
  274
+        else:
  275
+            if sid is None:
  276
+                # Roll back transaction
  277
+                connection.in_atomic_block = False
  278
+                try:
  279
+                    connection.rollback()
  280
+                finally:
  281
+                    if connection.features.autocommits_when_autocommit_is_off:
  282
+                        connection.autocommit = True
  283
+                    else:
  284
+                        connection.set_autocommit(True)
  285
+            else:
  286
+                # Roll back to savepoint
  287
+                connection.savepoint_rollback(sid)
  288
+
  289
+        # Remove this when the legacy transaction management goes away.
  290
+        self._legacy_leave_transaction_management(connection)
  291
+
  292
+
  293
+    def __call__(self, func):
  294
+        @wraps(func)
  295
+        def inner(*args, **kwargs):
  296
+            with self:
  297
+                return func(*args, **kwargs)
  298
+        return inner
  299
+
  300
+
  301
+def atomic(using=None):
  302
+    # Bare decorator: @atomic -- although the first argument is called
  303
+    # `using`, it's actually the function being decorated.
  304
+    if callable(using):
  305
+        return Atomic(DEFAULT_DB_ALIAS)(using)
  306
+    # Decorator: @atomic(...) or context manager: with atomic(...): ...
  307
+    else:
  308
+        return Atomic(using)
  309
+
  310
+
  311
+############################################
  312
+# Deprecated decorators / context managers #
  313
+############################################
172 314
 
173 315
 class Transaction(object):
174 316
     """
@@ -279,7 +421,8 @@ def commit_on_success_unless_managed(using=None):
279 421
     """
280 422
     Transitory API to preserve backwards-compatibility while refactoring.
281 423
     """
282  
-    if get_autocommit(using):
  424
+    connection = get_connection(using)
  425
+    if connection.autocommit and not connection.in_atomic_block:
283 426
         return commit_on_success(using)
284 427
     else:
285 428
         def entering(using):
97  docs/topics/db/transactions.txt
... ...
@@ -1,13 +1,16 @@
1  
-==============================
2  
-Managing database transactions
3  
-==============================
  1
+=====================
  2
+Database transactions
  3
+=====================
4 4
 
5 5
 .. module:: django.db.transaction
6 6
 
7 7
 Django gives you a few ways to control how database transactions are managed.
8 8
 
  9
+Managing database transactions
  10
+==============================
  11
+
9 12
 Django's default transaction behavior
10  
-=====================================
  13
+-------------------------------------
11 14
 
12 15
 Django's default behavior is to run in autocommit mode. Each query is
13 16
 immediately committed to the database. :ref:`See below for details
@@ -24,7 +27,7 @@ immediately committed to the database. :ref:`See below for details
24 27
     behavior <transactions-changes-from-1.5>`.
25 28
 
26 29
 Tying transactions to HTTP requests
27  
-===================================
  30
+-----------------------------------
28 31
 
29 32
 The recommended way to handle transactions in Web requests is to tie them to
30 33
 the request and response phases via Django's ``TransactionMiddleware``.
@@ -63,6 +66,85 @@ connection internally.
63 66
     multiple databases and want transaction control over databases other than
64 67
     "default", you will need to write your own transaction middleware.
65 68
 
  69
+Controlling transactions explicitly
  70
+-----------------------------------
  71
+
  72
+.. versionadded:: 1.6
  73
+
  74
+Django provides a single API to control database transactions.
  75
+
  76
+.. function:: atomic(using=None)
  77
+
  78
+    This function creates an atomic block for writes to the database.
  79
+    (Atomicity is the defining property of database transactions.)
  80
+
  81
+    When the block completes successfully, the changes are committed to the
  82
+    database. When it raises an exception, the changes are rolled back.
  83
+
  84
+    ``atomic`` can be nested. In this case, when an inner block completes
  85
+    successfully, its effects can still be rolled back if an exception is
  86
+    raised in the outer block at a later point.
  87
+
  88
+    ``atomic`` takes a ``using`` argument which should be the name of a
  89
+    database. If this argument isn't provided, Django uses the ``"default"``
  90
+    database.
  91
+
  92
+    ``atomic`` is usable both as a decorator::
  93
+
  94
+        from django.db import transaction
  95
+
  96
+        @transaction.atomic
  97
+        def viewfunc(request):
  98
+            # This code executes inside a transaction.
  99
+            do_stuff()
  100
+
  101
+    and as a context manager::
  102
+
  103
+        from django.db import transaction
  104
+
  105
+        def viewfunc(request):
  106
+            # This code executes in autocommit mode (Django's default).
  107
+            do_stuff()
  108
+
  109
+            with transaction.atomic():
  110
+                # This code executes inside a transaction.
  111
+                do_more_stuff()
  112
+
  113
+    Wrapping ``atomic`` in a try/except block allows for natural handling of
  114
+    integrity errors::
  115
+
  116
+        from django.db import IntegrityError, transaction
  117
+
  118
+        @transaction.atomic
  119
+        def viewfunc(request):
  120
+            do_stuff()
  121
+
  122
+            try:
  123
+                with transaction.atomic():
  124
+                    do_stuff_that_could_fail()
  125
+            except IntegrityError:
  126
+                handle_exception()
  127
+
  128
+            do_more_stuff()
  129
+
  130
+    In this example, even if ``do_stuff_that_could_fail()`` causes a database
  131
+    error by breaking an integrity constraint, you can execute queries in
  132
+    ``do_more_stuff()``, and the changes from ``do_stuff()`` are still there.
  133
+
  134
+    In order to guarantee atomicity, ``atomic`` disables some APIs. Attempting
  135
+    to commit, roll back, or change the autocommit state of the database
  136
+    connection within an ``atomic`` block will raise an exception.
  137
+
  138
+    ``atomic`` can only be used in autocommit mode. It will raise an exception
  139
+    if autocommit is turned off.
  140
+
  141
+    Under the hood, Django's transaction management code:
  142
+
  143
+    - opens a transaction when entering the outermost ``atomic`` block;
  144
+    - creates a savepoint when entering an inner ``atomic`` block;
  145
+    - releases or rolls back to the savepoint when exiting an inner block;
  146
+    - commits or rolls back the transaction when exiting the outermost block.
  147
+
66 148
 .. _transaction-management-functions:
67 149
 
68 150
 Controlling transaction management in views
@@ -325,9 +407,8 @@ When autocommit is enabled, savepoints don't make sense. When it's disabled,
325 407
 commits before any statement other than ``SELECT``, ``INSERT``, ``UPDATE``,
326 408
 ``DELETE`` and ``REPLACE``.)
327 409
 
328  
-As a consequence, savepoints are only usable if you start a transaction
329  
-manually while in autocommit mode, and Django doesn't provide an API to
330  
-achieve that.
  410
+As a consequence, savepoints are only usable inside a transaction ie. inside
  411
+an :func:`atomic` block.
331 412
 
332 413
 Transactions in MySQL
333 414
 ---------------------
2  tests/transactions/models.py
@@ -22,4 +22,4 @@ class Meta:
22 22
         ordering = ('first_name', 'last_name')
23 23
 
24 24
     def __str__(self):
25  
-        return "%s %s" % (self.first_name, self.last_name)
  25
+        return ("%s %s" % (self.first_name, self.last_name)).strip()
154  tests/transactions/tests.py
... ...
@@ -1,11 +1,163 @@
1 1
 from __future__ import absolute_import
2 2
 
  3
+import sys
  4
+
3 5
 from django.db import connection, transaction, IntegrityError
4  
-from django.test import TransactionTestCase, skipUnlessDBFeature
  6
+from django.test import TestCase, TransactionTestCase, skipUnlessDBFeature
  7
+from django.utils import six
  8
+from django.utils.unittest import skipUnless
5 9
 
6 10
 from .models import Reporter
7 11
 
8 12
 
  13
+@skipUnless(connection.features.uses_savepoints,
  14
+        "'atomic' requires transactions and savepoints.")
  15
+class AtomicTests(TransactionTestCase):
  16
+    """
  17
+    Tests for the atomic decorator and context manager.
  18
+
  19
+    The tests make assertions on internal attributes because there isn't a
  20
+    robust way to ask the database for its current transaction state.
  21
+
  22
+    Since the decorator syntax is converted into a context manager (see the
  23
+    implementation), there are only a few basic tests with the decorator
  24
+    syntax and the bulk of the tests use the context manager syntax.
  25
+    """
  26
+
  27
+    def test_decorator_syntax_commit(self):
  28
+        @transaction.atomic
  29
+        def make_reporter():
  30
+            Reporter.objects.create(first_name="Tintin")
  31
+        make_reporter()
  32
+        self.assertQuerysetEqual(Reporter.objects.all(), ['<Reporter: Tintin>'])
  33
+
  34
+    def test_decorator_syntax_rollback(self):
  35
+        @transaction.atomic
  36
+        def make_reporter():
  37
+            Reporter.objects.create(first_name="Haddock")
  38
+            raise Exception("Oops, that's his last name")
  39
+        with six.assertRaisesRegex(self, Exception, "Oops"):
  40
+            make_reporter()
  41
+        self.assertQuerysetEqual(Reporter.objects.all(), [])
  42
+
  43
+    def test_alternate_decorator_syntax_commit(self):
  44
+        @transaction.atomic()
  45
+        def make_reporter():
  46
+            Reporter.objects.create(first_name="Tintin")
  47
+        make_reporter()
  48
+        self.assertQuerysetEqual(Reporter.objects.all(), ['<Reporter: Tintin>'])
  49
+
  50
+    def test_alternate_decorator_syntax_rollback(self):
  51
+        @transaction.atomic()
  52
+        def make_reporter():
  53
+            Reporter.objects.create(first_name="Haddock")
  54
+            raise Exception("Oops, that's his last name")
  55
+        with six.assertRaisesRegex(self, Exception, "Oops"):
  56
+            make_reporter()
  57
+        self.assertQuerysetEqual(Reporter.objects.all(), [])
  58
+
  59
+    def test_commit(self):
  60
+        with transaction.atomic():
  61
+            Reporter.objects.create(first_name="Tintin")
  62
+        self.assertQuerysetEqual(Reporter.objects.all(), ['<Reporter: Tintin>'])
  63
+
  64
+    def test_rollback(self):
  65
+        with six.assertRaisesRegex(self, Exception, "Oops"):
  66
+            with transaction.atomic():
  67
+                Reporter.objects.create(first_name="Haddock")
  68
+                raise Exception("Oops, that's his last name")
  69
+        self.assertQuerysetEqual(Reporter.objects.all(), [])
  70
+
  71
+    def test_nested_commit_commit(self):
  72
+        with transaction.atomic():
  73
+            Reporter.objects.create(first_name="Tintin")
  74
+            with transaction.atomic():
  75
+                Reporter.objects.create(first_name="Archibald", last_name="Haddock")
  76
+        self.assertQuerysetEqual(Reporter.objects.all(),
  77
+                ['<Reporter: Archibald Haddock>', '<Reporter: Tintin>'])
  78
+
  79
+    def test_nested_commit_rollback(self):
  80
+        with transaction.atomic():
  81
+            Reporter.objects.create(first_name="Tintin")
  82
+            with six.assertRaisesRegex(self, Exception, "Oops"):
  83
+                with transaction.atomic():
  84
+                    Reporter.objects.create(first_name="Haddock")
  85
+                    raise Exception("Oops, that's his last name")
  86
+        self.assertQuerysetEqual(Reporter.objects.all(), ['<Reporter: Tintin>'])
  87
+
  88
+    def test_nested_rollback_commit(self):
  89
+        with six.assertRaisesRegex(self, Exception, "Oops"):
  90
+            with transaction.atomic():
  91
+                Reporter.objects.create(last_name="Tintin")
  92
+                with transaction.atomic():
  93
+                    Reporter.objects.create(last_name="Haddock")
  94
+                raise Exception("Oops, that's his first name")
  95
+        self.assertQuerysetEqual(Reporter.objects.all(), [])
  96
+
  97
+    def test_nested_rollback_rollback(self):
  98
+        with six.assertRaisesRegex(self, Exception, "Oops"):
  99
+            with transaction.atomic():
  100
+                Reporter.objects.create(last_name="Tintin")
  101
+                with six.assertRaisesRegex(self, Exception, "Oops"):
  102
+                    with transaction.atomic():
  103
+                        Reporter.objects.create(first_name="Haddock")
  104
+                    raise Exception("Oops, that's his last name")
  105
+                raise Exception("Oops, that's his first name")
  106
+        self.assertQuerysetEqual(Reporter.objects.all(), [])
  107
+
  108
+    def test_reuse_commit_commit(self):
  109
+        atomic = transaction.atomic()
  110
+        with atomic:
  111
+            Reporter.objects.create(first_name="Tintin")
  112
+            with atomic:
  113
+                Reporter.objects.create(first_name="Archibald", last_name="Haddock")
  114
+        self.assertQuerysetEqual(Reporter.objects.all(),
  115
+                ['<Reporter: Archibald Haddock>', '<Reporter: Tintin>'])
  116
+
  117
+    def test_reuse_commit_rollback(self):
  118
+        atomic = transaction.atomic()
  119
+        with atomic:
  120
+            Reporter.objects.create(first_name="Tintin")
  121
+            with six.assertRaisesRegex(self, Exception, "Oops"):
  122
+                with atomic:
  123
+                    Reporter.objects.create(first_name="Haddock")
  124
+                    raise Exception("Oops, that's his last name")
  125
+        self.assertQuerysetEqual(Reporter.objects.all(), ['<Reporter: Tintin>'])
  126
+
  127
+    def test_reuse_rollback_commit(self):
  128
+        atomic = transaction.atomic()
  129
+        with six.assertRaisesRegex(self, Exception, "Oops"):
  130
+            with atomic:
  131
+                Reporter.objects.create(last_name="Tintin")
  132
+                with atomic:
  133
+                    Reporter.objects.create(last_name="Haddock")
  134
+                raise Exception("Oops, that's his first name")
  135
+        self.assertQuerysetEqual(Reporter.objects.all(), [])
  136
+
  137
+    def test_reuse_rollback_rollback(self):
  138
+        atomic = transaction.atomic()
  139
+        with six.assertRaisesRegex(self, Exception, "Oops"):
  140
+            with atomic:
  141
+                Reporter.objects.create(last_name="Tintin")
  142
+                with six.assertRaisesRegex(self, Exception, "Oops"):
  143
+                    with atomic:
  144
+                        Reporter.objects.create(first_name="Haddock")
  145
+                    raise Exception("Oops, that's his last name")
  146
+                raise Exception("Oops, that's his first name")
  147
+        self.assertQuerysetEqual(Reporter.objects.all(), [])
  148
+
  149
+
  150
+class AtomicInsideTransactionTests(AtomicTests):
  151
+    """All basic tests for atomic should also pass within an existing transaction."""
  152
+
  153
+    def setUp(self):
  154
+        self.atomic = transaction.atomic()
  155
+        self.atomic.__enter__()
  156
+
  157
+    def tearDown(self):
  158
+        self.atomic.__exit__(*sys.exc_info())
  159
+
  160
+
9 161
 class TransactionTests(TransactionTestCase):
10 162
     def create_a_reporter_then_fail(self, first, last):
11 163
         a = Reporter(first_name=first, last_name=last)

0 notes on commit d7bc4fb

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