1515 from typing import NoReturn
1616
1717
18- def dbs_with_open_transactions () -> frozenset [str ]:
19- """
20- Get the names of databases with open transactions.
21- """
22- dbs_with_open_transaction = set ()
23- # Note: django_db.connections is a special class which implements __iter__,
24- # and should not be confused with a list or dict.
25- for db_alias in django_db .connections :
26- if in_transaction (using = db_alias ):
27- dbs_with_open_transaction .add (db_alias )
28-
29- return frozenset (dbs_with_open_transaction )
30-
31-
32- def durable [** P , R ](func : Callable [P , R ]) -> Callable [P , R ]:
33- """
34- Enforce durability with this decorator.
35-
36- "Durability" means that the function's work cannot be rolled back after it completes,
37- and is not to be confused with "atomicity" (which is about ensuring that the function
38- either completes all its work or none of it).
39-
40- We enforce this by ensuring that the function is not called within a transaction,
41- and that no transaction is left open when the function completes.
42-
43- Raises:
44- _UnexpectedOpenTransaction: if a transaction is already open when this is called.
45- _UnexpectedDanglingTransaction: if a transaction remains open after the decorated
46- function exits. Before raising this exeption, we roll back and end the
47- transaction.
48- """
49-
50- @functools .wraps (func )
51- def wrapper (* args : P .args , ** kwargs : P .kwargs ) -> R :
52- if open_dbs := dbs_with_open_transactions ():
53- raise _UnexpectedOpenTransaction (open_dbs = open_dbs )
54-
55- return_value = func (* args , ** kwargs )
56-
57- if open_dbs := dbs_with_open_transactions ():
58- # Clean up first, otherwise we may see errors later that will mask this one.
59- # This can only happen if the function manually opens a transaction,
60- # so we need to manually roll it back and close it.
61- for db_alias in open_dbs :
62- django_transaction .rollback (using = db_alias )
63- django_transaction .set_autocommit (True , using = db_alias )
64- raise _UnexpectedDanglingTransaction (open_dbs = open_dbs )
65-
66- return return_value
67-
68- return wrapper
69-
70-
71- @contextlib .contextmanager
72- def transaction_required (* , using : str | None = None ) -> Iterator [None ]:
73- """
74- Make sure that code is always executed in a transaction.
75-
76- Can be used as a decorator or a context manager.
77-
78- We ignore test-suite transactions when checking for a transaction
79- because we don't want to run the risk of allowing code to pass tests
80- but fail in production.
81-
82- See Note [_MissingRequiredTransaction in tests]
83-
84- Raises:
85- _MissingRequiredTransaction: if we are not in a transaction.
86- """
87- if using is None :
88- using = django_db .DEFAULT_DB_ALIAS
89-
90- if not in_transaction (using = using ):
91- raise _MissingRequiredTransaction (database = using )
92- yield
93-
94-
9518@contextlib .contextmanager
9619def transaction (* , using : str | None = None ) -> Iterator [None ]:
9720 """
9821 Create a database transaction.
9922
10023 Nested calls are not allowed because SQL does not support nested transactions.
101- Consider this like `atomic(durable=True)`, but with added after-commit callback support in tests.
24+ Consider this like Django's `atomic(durable=True)`, but with added after-commit callback support in tests.
10225
10326 This wraps Django's 'atomic' function.
10427
@@ -140,7 +63,7 @@ def transaction_if_not_already(*, using: str | None = None) -> Iterator[None]:
14063 yield
14164
14265
143- class NotADecorator (Exception ):
66+ class _NotADecorator (Exception ):
14467 """
14568 Raised when a context manager is mistakenly used as a decorator.
14669 """
@@ -157,7 +80,7 @@ class _NonDecoratorContextManager[T_Co](contextlib._GeneratorContextManager[T_Co
15780 """
15881
15982 def __call__ (self , func : object ) -> NoReturn :
160- raise NotADecorator
83+ raise _NotADecorator
16184
16285
16386def _contextmanager_without_decorator [** P , T_Co ](
@@ -185,11 +108,12 @@ def savepoint(*, using: str | None = None) -> Generator[None, None, None]:
185108 Must be called inside an active transaction.
186109
187110 Tips:
111+
188112 - You should only create a savepoint if you may roll back to it before
189113 continuing with your transaction. If your intention is to ensure that
190114 your code is committed atomically, consider using `transaction_required`
191115 instead.
192- - Savepoint rollback should be handled _where we create the savepoint_ .
116+ - We believe savepoint rollback should be handled where the savepoint is created .
193117 That locality is not possible with a decorator, so this function
194118 deliberately does not work as one.
195119
@@ -204,6 +128,69 @@ def savepoint(*, using: str | None = None) -> Generator[None, None, None]:
204128 yield
205129
206130
131+ @contextlib .contextmanager
132+ def transaction_required (* , using : str | None = None ) -> Iterator [None ]:
133+ """
134+ Make sure that code is always executed in a transaction.
135+
136+ Can be used as a decorator or a context manager.
137+
138+ We ignore test-suite transactions when checking for a transaction
139+ because we don't want to run the risk of allowing code to pass tests
140+ but fail in production.
141+
142+ See Note [_MissingRequiredTransaction in tests]
143+
144+ Raises:
145+ _MissingRequiredTransaction: if we are not in a transaction.
146+ """
147+ if using is None :
148+ using = django_db .DEFAULT_DB_ALIAS
149+
150+ if not in_transaction (using = using ):
151+ raise _MissingRequiredTransaction (database = using )
152+ yield
153+
154+
155+ def durable [** P , R ](func : Callable [P , R ]) -> Callable [P , R ]:
156+ """
157+ Enforce durability with this decorator.
158+
159+ "Durability" means that the function's work cannot be rolled back after it completes,
160+ and is not to be confused with "atomicity" (which is about ensuring that the function
161+ either completes all its work or none of it).
162+
163+ We enforce this by ensuring that the function is not called within a transaction,
164+ and that no transaction is left open when the function completes.
165+
166+ Raises:
167+ _UnexpectedOpenTransaction: if a transaction is already open when this is called.
168+ _UnexpectedDanglingTransaction: if a transaction remains open after the decorated
169+ function exits. Before raising this exeption, we roll back and end the
170+ transaction.
171+ """
172+
173+ @functools .wraps (func )
174+ def wrapper (* args : P .args , ** kwargs : P .kwargs ) -> R :
175+ if open_dbs := dbs_with_open_transactions ():
176+ raise _UnexpectedOpenTransaction (open_dbs = open_dbs )
177+
178+ return_value = func (* args , ** kwargs )
179+
180+ if open_dbs := dbs_with_open_transactions ():
181+ # Clean up first, otherwise we may see errors later that will mask this one.
182+ # This can only happen if the function manually opens a transaction,
183+ # so we need to manually roll it back and close it.
184+ for db_alias in open_dbs :
185+ django_transaction .rollback (using = db_alias )
186+ django_transaction .set_autocommit (True , using = db_alias )
187+ raise _UnexpectedDanglingTransaction (open_dbs = open_dbs )
188+
189+ return return_value
190+
191+ return wrapper
192+
193+
207194@contextlib .contextmanager
208195def _execute_on_commit_callbacks_in_tests (using : str | None = None ) -> Iterator [None ]:
209196 """
@@ -428,3 +415,17 @@ def in_transaction(*, using: str | None = None) -> bool:
428415 return False
429416 else :
430417 return True
418+
419+
420+ def dbs_with_open_transactions () -> frozenset [str ]:
421+ """
422+ Get the names of databases with open transactions.
423+ """
424+ dbs_with_open_transaction = set ()
425+ # Note: django_db.connections is a special class which implements __iter__,
426+ # and should not be confused with a list or dict.
427+ for db_alias in django_db .connections :
428+ if in_transaction (using = db_alias ):
429+ dbs_with_open_transaction .add (db_alias )
430+
431+ return frozenset (dbs_with_open_transaction )
0 commit comments