@@ -63,6 +63,9 @@ def _transaction(*, using: str | None) -> Generator[None]:
6363 # the `savepoint` flag is ignored when `durable` is `True`.
6464 django_transaction .atomic (using = using , durable = True ),
6565 ):
66+ connection = django_transaction .get_connection (using = using )
67+ atomic_block = connection .atomic_blocks [- 1 ]
68+ atomic_block ._from_subatomic = True # noqa: SLF001
6669 yield
6770
6871 decorator = _transaction (using = using )
@@ -325,6 +328,37 @@ class _MissingRequiredTransaction(Exception):
325328 database : str
326329
327330
331+ @attrs .frozen
332+ class _AmbiguousAfterCommitTestBehaviour (Exception ):
333+ """
334+ Raised in tests when it's unclear if after-commit callbacks should be run.
335+
336+ You are calling `run_after_commit` inside an `atomic` block in a test.
337+ This `atomic` is inside the test suite's transaction,
338+ so after-commit callbacks will not be run
339+ (because the test suite will roll back the transaction).
340+
341+ Subatomic doesn't know if you intended for these callbacks to be run or not,
342+ so raises this error to avoid silently doing the wrong thing.
343+
344+ In production code, or tests where after-commit callbacks should be run,
345+ replace (or wrap) the `atomic` block with `subatomic.db.transaction`.
346+
347+ In tests where after-commit callbacks should not be run,
348+ use `subatomic.test.part_of_a_transaction` instead.
349+
350+ To help your project progressively adopt this check,
351+ you can disable this requirement for after-commit callbacks by setting
352+ `settings.SUBATOMIC_AFTER_COMMIT_AMBIGUITY_ERROR_IN_TESTS` to `False`.
353+
354+ See Note [_MissingRequiredTransaction in tests]
355+
356+ This exception should not be caught, as it indicates a programming error.
357+ """
358+
359+ database : str
360+
361+
328362@attrs .frozen
329363class _UnexpectedOpenTransaction (Exception ):
330364 """
@@ -424,18 +458,9 @@ def run_after_commit(
424458 if using is None :
425459 using = django_db .DEFAULT_DB_ALIAS
426460
427- # See Note [After-commit callbacks require a transaction]
428- needs_transaction = getattr (
429- settings , "SUBATOMIC_AFTER_COMMIT_NEEDS_TRANSACTION" , True
430- )
431- only_in_testcase_transaction = _innermost_atomic_block_wraps_testcase (using = using )
432-
433- # Fail if a transaction is required, but none exists.
434- # Ignore test-suite transactions when checking for a transaction.
435- # See Note [After-commit callbacks require a transaction]
436- if needs_transaction and not in_transaction (using = using ):
437- raise _MissingRequiredTransaction (database = using )
461+ _ensure_transaction_is_open (using = using )
438462
463+ only_in_testcase_transaction = _innermost_atomic_block_wraps_testcase (using = using )
439464 if (
440465 # See Note [Running after-commit callbacks in tests]
441466 getattr (settings , "SUBATOMIC_RUN_AFTER_COMMIT_CALLBACKS_IN_TESTS" , True )
@@ -446,6 +471,54 @@ def run_after_commit(
446471 django_transaction .on_commit (callback , using = using )
447472
448473
474+ def _ensure_transaction_is_open (* , using : str ) -> None :
475+ """
476+ Raise an error if transactions are required but missing.
477+
478+ If there is no transaction open, `_MissingRequiredTransaction` is raised.
479+ This can be silenced with the Django setting
480+ `SUBATOMIC_AFTER_COMMIT_NEEDS_TRANSACTION`.
481+
482+ See Note [After-commit callbacks require a transaction]
483+
484+ When transactions are managed by the test suite,
485+ this also ensures after-commit emulation is accounted for by Subatomic.
486+ When they are not, `_AmbiguousAfterCommitTestBehaviour` is raised.
487+ This can be silenced with the Django setting
488+ `SUBATOMIC_AFTER_COMMIT_AMBIGUITY_ERROR_IN_TESTS`.
489+ """
490+ needs_transaction = getattr (
491+ settings , "SUBATOMIC_AFTER_COMMIT_NEEDS_TRANSACTION" , True
492+ )
493+ # Skip checks if they have been disabled.
494+ if not needs_transaction :
495+ return
496+
497+ # Fail if we're not in a transaction.
498+ if not in_transaction (using = using ):
499+ raise _MissingRequiredTransaction (database = using )
500+
501+ ambiguity_error_in_tests = getattr (
502+ settings , "SUBATOMIC_AFTER_COMMIT_AMBIGUITY_ERROR_IN_TESTS" , True
503+ )
504+ if not ambiguity_error_in_tests :
505+ return
506+
507+ connection = django_transaction .get_connection (using = using )
508+
509+ # We expect after-commit callbacks to be handled by Django
510+ # if we're not in a test-managed transaction.
511+ if not connection .atomic_blocks [0 ]._from_testcase : # noqa: SLF001
512+ return
513+
514+ # `_from_testcase` told us that we're in a test-managed transaction.
515+ # `in_transaction` told us that we're in a further atomic context.
516+ # If Subatomic didn't open that context then we don't know if the
517+ # test expects after-commit callbacks to be emulated or not.
518+ if not hasattr (connection .atomic_blocks [1 ], "_from_subatomic" ):
519+ raise _AmbiguousAfterCommitTestBehaviour (database = using )
520+
521+
449522def _innermost_atomic_block_wraps_testcase (* , using : str | None = None ) -> bool :
450523 """
451524 Return True if the current innermost atomic block is wrapping a test case.
0 commit comments