@@ -194,6 +194,100 @@ def raises() -> None:
194194 assert error_raised is True
195195 assert counter .count == 1
196196
197+ @pytest .mark .parametrize (
198+ "transaction_manager" ,
199+ (db .transaction , db .transaction_if_not_already ),
200+ )
201+ def test_unhandled_callbacks_cause_error (
202+ self , transaction_manager : DBContextManager
203+ ) -> None :
204+ """
205+ If callbacks from a previous atomic context remain, raise an error.
206+ """
207+ counter = Counter ()
208+
209+ # Django's `atomic` leaves unhandled after-commit actions on exit.
210+ with django_transaction .atomic ():
211+ db .run_after_commit (counter .increment )
212+
213+ # `transaction` will raise when it finds the unhandled callback.
214+ with pytest .raises (db ._UnhandledCallbacks ) as exc_info : # noqa: SLF001
215+ with transaction_manager ():
216+ ...
217+
218+ assert counter .count == 0
219+ assert exc_info .value .callbacks == (counter .increment ,)
220+
221+ @pytest .mark .parametrize (
222+ "transaction_manager" ,
223+ (db .transaction , db .transaction_if_not_already ),
224+ )
225+ def test_unhandled_callbacks_check_can_be_disabled (
226+ self , transaction_manager : DBContextManager
227+ ) -> None :
228+ """
229+ We can disable the check for unhandled callbacks.
230+ """
231+ counter = Counter ()
232+
233+ # Django's `atomic` leaves unhandled after-commit actions on exit.
234+ with django_transaction .atomic ():
235+ db .run_after_commit (counter .increment )
236+
237+ # Run after-commit callbacks when `transaction` exits,
238+ # even if that means running them later than is realistic.
239+ with override_settings (SUBATOMIC_CATCH_UNHANDLED_AFTER_COMMIT_CALLBACKS = False ):
240+ with transaction_manager ():
241+ assert counter .count == 0
242+
243+ assert counter .count == 1
244+
245+ @pytest .mark .parametrize (
246+ "transaction_manager" ,
247+ (db .transaction , db .transaction_if_not_already ),
248+ )
249+ def test_handled_callbacks_are_not_an_error (
250+ self , transaction_manager : DBContextManager
251+ ) -> None :
252+ """
253+ Already-handled checks do not cause an error.
254+ """
255+ counter = Counter ()
256+
257+ # Callbacks are handled by `transaction` and removed from the queue.
258+ with db .transaction ():
259+ db .run_after_commit (counter .increment )
260+ assert counter .count == 0
261+ assert counter .count == 1
262+
263+ # The callbacks have been handled, so a second `transaction` does not raise.
264+ with transaction_manager ():
265+ pass
266+
267+ # The callback was not run a second time.
268+ assert counter .count == 1
269+
270+ @pytest .mark .parametrize (
271+ "transaction_manager" ,
272+ (db .transaction , db .transaction_if_not_already ),
273+ )
274+ def test_callbacks_ignored_by_transaction_if_not_already (
275+ self , transaction_manager : DBContextManager
276+ ) -> None :
277+ """
278+ `transaction_if_not_already` ignores after-commit callbacks if a transaction already exists.
279+ """
280+ counter = Counter ()
281+
282+ with transaction_manager ():
283+ db .run_after_commit (counter .increment )
284+ with db .transaction_if_not_already ():
285+ assert counter .count == 0
286+ assert counter .count == 0
287+
288+ # The callback is run when the outermost transaction exits.
289+ assert counter .count == 1
290+
197291
198292class TestTransactionRequired :
199293 @_parametrize_transaction_testcase
0 commit comments