Skip to content

Commit 89a4a4a

Browse files
authored
Merge pull request #60 from kraken-tech/api-docs-reshuffle
Tidy up generated API docs
2 parents 275b2b8 + 4f858d3 commit 89a4a4a

5 files changed

Lines changed: 100 additions & 83 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1111

1212
- Search plugin for MkDocs
1313

14+
### Removed
15+
16+
- `django_subatomic.db.NotADecorator` is no longer a part of the public API.
17+
It has been renamed to `_NotADecorator`.
18+
This implementation detail was not intended for general use,
19+
and may be removed in a future release.
20+
1421
## [0.1.1] - 2025-09-20
1522

1623
### Added

docs/why.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ Django's atomic creates many savepoints that are never used. There are a couple
2323

2424
1. Savepoints are created with decorators (`@atomic`).
2525
2. `atomic` creates savepoints by default. The default arguments (*Behaviour* **A**) are an [attractive nuisance](https://blog.ganssle.io/articles/2023/01/attractive-nuisances.html) because they make us create savepoints when we don't need them.
26+
2627
> … if you have two ways to accomplish a task and one is a simple way that *looks* like the right thing but is subtly wrong, and the other is correct but
2728
> more complicated, the majority of people will end up doing the wrong
2829
> thing.
2930
> [**Attractive nuisances in software design**](https://blog.ganssle.io/articles/2023/01/attractive-nuisances.html) - [Paul Ganssle](https://blog.ganssle.io/author/paul-ganssle.html)
31+
3032
3. We have no easy way to indicate the creation of a savepoint that doesn't have the potential to create a transaction instead. The only tool we have to create a savepoint is *Behaviour* **A**, which can create a transaction.
3133

3234
## What Subatomic implements

mkdocs.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,14 @@ extra:
3838
markdown_extensions:
3939
- tables
4040
plugins:
41-
- mkdocstrings
41+
- mkdocstrings:
42+
handlers:
43+
python:
44+
options:
45+
members_order: source
46+
separate_signature: true
47+
show_signature_annotations: true
48+
show_signature_type_parameters: true
4249
- api-autonav:
4350
modules: ['src/django_subatomic']
4451
- search

src/django_subatomic/db.py

Lines changed: 82 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -15,90 +15,13 @@
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
9619
def 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

16386
def _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
208195
def _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)

tests/test_db.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ def test_not_a_decorator(self) -> None:
202202
"""
203203
`savepoint` cannot be used as a decorator.
204204
"""
205-
with pytest.raises(db.NotADecorator):
205+
with pytest.raises(db._NotADecorator): # noqa: SLF001
206206

207207
@db.savepoint()
208208
def inner() -> None: ...

0 commit comments

Comments
 (0)