Skip to content

Commit 93c4785

Browse files
authored
Merge pull request #21 from kraken-tech/after-commit-transaction-required-setting
Control after-commit behaviour with a setting
2 parents 450d3ad + 1f224c2 commit 93c4785

File tree

3 files changed

+20
-53
lines changed

3 files changed

+20
-53
lines changed

pytest.ini

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,6 @@ addopts =
4848
markers =
4949
# See Note [Running after-commit callbacks in tests]
5050
deprecated_ignore_after_commit_callbacks: require after-commit callbacks to be explicitly captured and called
51-
# See Note [After-commit callbacks require a transaction]
52-
deprecated_run_after_commit_outside_transaction: relax rule where after-commit callbacks require a transaction
5351

5452
# Ensure passing xfailed (aka "xpass") tests cause the test suite to fail.
5553
# https://docs.pytest.org/en/latest/skipping.html#strict-parameter

src/django_subatomic/db.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import attrs
88
from django import db as django_db
9+
from django.conf import settings
910
from django.db import transaction as django_transaction
1011

1112

@@ -14,8 +15,6 @@
1415
from typing import NoReturn
1516

1617

17-
# See Note [After-commit callbacks require a transaction]
18-
_REQUIRE_TRANSACTION_FOR_AFTER_COMMIT_CALLBACKS = True
1918
# See Note [Running after-commit callbacks in tests]
2019
_RUN_AFTER_COMMIT_CALLBACKS_IN_TESTS = True
2120

@@ -323,6 +322,9 @@ class _UnexpectedDanglingTransaction(Exception):
323322
# We choose to disallow immediate execution because it can be misleading and hide bugs.
324323
# In particular, it can hide the fact that a transaction is missing or on a different database,
325324
# which can make code read as though a callback will be deferred when it won't be.
325+
#
326+
# To help projects migrate to this behaviour, this requirement can be disabled with
327+
# the Django setting `SUBATOMIC_AFTER_COMMIT_NEEDS_TRANSACTION`.
326328

327329

328330
# Note [Running after-commit callbacks in tests]
@@ -354,7 +356,10 @@ def run_after_commit(
354356
if using is None:
355357
using = django_db.DEFAULT_DB_ALIAS
356358

357-
needs_transaction = _REQUIRE_TRANSACTION_FOR_AFTER_COMMIT_CALLBACKS
359+
# See Note [After-commit callbacks require a transaction]
360+
needs_transaction = getattr(
361+
settings, "SUBATOMIC_AFTER_COMMIT_NEEDS_TRANSACTION", True
362+
)
358363
only_in_testcase_transaction = _innermost_atomic_block_wraps_testcase(using=using)
359364

360365
# Fail if a transaction is required, but none exists.

tests/test_db.py

Lines changed: 12 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import pytest
88
from django import db as django_db
99
from django.db import transaction as django_transaction
10+
from django.test import override_settings
1011

1112
from django_subatomic import db, test
1213

@@ -59,32 +60,6 @@ def _parametrize_transaction_testcase(func: Callable[..., None]) -> MarkDecorato
5960
return parametrize(usefixtures(func)) # type: ignore[no-any-return]
6061

6162

62-
@pytest.fixture(autouse=True)
63-
def _require_transactions_for_after_commit_callbacks(
64-
request: pytest.FixtureRequest,
65-
) -> Generator[None]:
66-
"""
67-
Ensure after-commit callbacks are executed from within a transaction.
68-
69-
This ensures that we do not accidentally write code which looks like it defers execution,
70-
but is actually running immediately.
71-
72-
This fixture enables that new behaviour for all new tests by default,
73-
but offers an escape-hatch for tests which have not yet been fixed.
74-
75-
TODO: Allow users of this library to do this in their own tests.
76-
77-
See Note [After-commit callbacks require a transaction].
78-
"""
79-
if "deprecated_run_after_commit_outside_transaction" in request.keywords:
80-
with mock.patch.object(
81-
db, "_REQUIRE_TRANSACTION_FOR_AFTER_COMMIT_CALLBACKS", new=False
82-
):
83-
yield
84-
else:
85-
yield
86-
87-
8863
@pytest.fixture(autouse=True)
8964
def _prevent_after_commit_callbacks(
9065
request: pytest.FixtureRequest,
@@ -387,6 +362,10 @@ def test_unclosed_manual_transaction(self, db_name: str) -> None:
387362

388363

389364
class TestRunAfterCommit:
365+
"""
366+
Tests for `run_after_commit`.
367+
"""
368+
390369
@_parametrize_transaction_testcase
391370
def test_outside_transaction_error(self) -> None:
392371
"""
@@ -403,18 +382,16 @@ def test_outside_transaction_error(self) -> None:
403382
assert counter.count == 0
404383

405384
@_parametrize_transaction_testcase
406-
@pytest.mark.deprecated_run_after_commit_outside_transaction
407-
def test_executes_immediately(self) -> None:
385+
def test_transaction_check_can_be_disabled(self) -> None:
408386
"""
409-
`run_after_commit` executes the callback immediately if there is no transaction...
410-
411-
... and if the test is marked with `deprecated_run_after_commit_outside_transaction`.
387+
Callbacks run immediately if there is no transaction and `SUBATOMIC_AFTER_COMMIT_NEEDS_TRANSACTION` is False.
412388
413389
See Note [After-commit callbacks require a transaction]
414390
"""
415391
counter = Counter()
416392

417-
db.run_after_commit(counter.increment)
393+
with override_settings(SUBATOMIC_AFTER_COMMIT_NEEDS_TRANSACTION=False):
394+
db.run_after_commit(counter.increment)
418395

419396
assert counter.count == 1
420397

@@ -487,29 +464,16 @@ def test_not_executed_if_rolled_back(self) -> None:
487464

488465

489466
class TestRunAfterCommitDeprecatedTestBehaviour:
490-
@pytest.mark.deprecated_run_after_commit_outside_transaction
491-
def test_does_not_raise_error_when_marked(self) -> None:
492-
"""
493-
No error is raised in test marked with `deprecated_run_after_commit_outside_transaction`.
494-
495-
Note: this would usually fail because we're enqueuing an after-commit callback without a
496-
transaction.
497-
"""
498-
try:
499-
db.run_after_commit(object)
500-
except Exception: # noqa: BLE001
501-
pytest.fail("An error should not have been raised.")
502-
503467
@pytest.mark.deprecated_ignore_after_commit_callbacks
504-
@pytest.mark.deprecated_run_after_commit_outside_transaction
468+
@override_settings(SUBATOMIC_AFTER_COMMIT_NEEDS_TRANSACTION=False)
505469
def test_does_not_execute_when_in_testcase_transaction_if_callbacks_disabled(
506470
self,
507471
) -> None:
508472
"""
509473
`run_after_commit` should not ignore testcase transactions when test marked with `deprecated_ignore_after_commit_callbacks`.
510474
511-
Note: this test also requires `deprecated_run_after_commit_outside_transaction`,
512-
because otherwise an error would be raised when trying to run the callback.
475+
We have to disable `SUBATOMIC_AFTER_COMMIT_NEEDS_TRANSACTION`, because
476+
otherwise an error would be raised when trying to register the callback.
513477
"""
514478
counter = Counter()
515479

0 commit comments

Comments
 (0)