Skip to content

Commit fc64c57

Browse files
committed
Add shared_db_wrapper for creating long-lived db state
1 parent d3e03b9 commit fc64c57

File tree

3 files changed

+87
-4
lines changed

3 files changed

+87
-4
lines changed

pytest_django/fixtures.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
from __future__ import with_statement
44

5+
from contextlib import contextmanager
56
import os
7+
import sys
68
import warnings
79

810
import pytest
@@ -13,7 +15,8 @@
1315
from .django_compat import is_django_unittest
1416
from .lazy_django import get_django_version, skip_if_no_django
1517

16-
__all__ = ['_django_db_setup', 'db', 'transactional_db', 'admin_user',
18+
__all__ = ['_django_db_setup', 'db', 'transactional_db', 'shared_db_wrapper',
19+
'admin_user',
1720
'django_user_model', 'django_username_field',
1821
'client', 'admin_client', 'rf', 'settings', 'live_server',
1922
'_live_server_helper']
@@ -192,6 +195,62 @@ def transactional_db(request, _django_db_setup, _django_cursor_wrapper):
192195
return _django_db_fixture_helper(True, request, _django_cursor_wrapper)
193196

194197

198+
@pytest.fixture(scope='session')
199+
def shared_db_wrapper(_django_db_setup, _django_cursor_wrapper):
200+
"""Wrapper for common database initialization code.
201+
202+
This fixture provides a context manager that let's you access the database
203+
from a transaction spanning multiple tests.
204+
"""
205+
from django.db import connection, transaction
206+
207+
if get_django_version() < (1, 6):
208+
raise Exception('shared_db_wrapper is only supported on Django >= 1.6.')
209+
210+
class DummyException(Exception):
211+
"""Dummy for use with Atomic.__exit__."""
212+
213+
@contextmanager
214+
def wrapper(request):
215+
# We need to take the request
216+
# to bind finalization to the place where this is used
217+
if 'transactional_db' in request.funcargnames:
218+
raise Exception(
219+
'shared_db_wrapper cannot be used with `transactional_db`.')
220+
221+
with _django_cursor_wrapper:
222+
if not connection.features.supports_transactions:
223+
raise Exception(
224+
"shared_db_wrapper cannot be used when "
225+
"the database doesn't support transactions.")
226+
227+
exc_type, exc_value, traceback = DummyException, DummyException(), None
228+
# Use atomic instead of calling .savepoint* directly.
229+
# This way works for both top-level transactions and "subtransactions".
230+
atomic = transaction.atomic()
231+
232+
def finalize():
233+
# Only run __exit__ if there was no error running the wrapped function.
234+
# Otherwise we've run it already.
235+
if exc_type == DummyException:
236+
# dummy exception makes `atomic` rollback the savepoint
237+
atomic.__exit__(exc_type, exc_value, traceback)
238+
239+
try:
240+
_django_cursor_wrapper.enable()
241+
atomic.__enter__()
242+
yield
243+
except:
244+
exc_type, exc_value, traceback = sys.exc_info()
245+
atomic.__exit__(exc_type, exc_value, traceback)
246+
raise
247+
finally:
248+
request.addfinalizer(finalize)
249+
_django_cursor_wrapper.restore()
250+
251+
return wrapper
252+
253+
195254
@pytest.fixture()
196255
def client():
197256
"""A Django test client instance."""

pytest_django/plugin.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@
1717
from .django_compat import is_django_unittest
1818
from .fixtures import (_django_db_setup, _live_server_helper, admin_client,
1919
admin_user, client, db, django_user_model,
20-
django_username_field, live_server, rf, settings,
21-
transactional_db)
20+
django_username_field, live_server, rf, shared_db_wrapper,
21+
settings, transactional_db)
2222
from .lazy_django import django_settings_is_configured, skip_if_no_django
2323

2424
# Silence linters for imported fixtures.
2525
(_django_db_setup, _live_server_helper, admin_client, admin_user, client, db,
2626
django_user_model, django_username_field, live_server, rf, settings,
27-
transactional_db)
27+
shared_db_wrapper, transactional_db)
2828

2929

3030
SETTINGS_MODULE_ENV = 'DJANGO_SETTINGS_MODULE'

tests/test_database.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from django.db import connection, transaction
55
from django.test.testcases import connections_support_transactions
66

7+
from pytest_django.lazy_django import get_django_version
78
from pytest_django_test.app.models import Item
89

910

@@ -51,6 +52,29 @@ def test_noaccess_fixture(noaccess):
5152
pass
5253

5354

55+
@pytest.mark.skipif(get_django_version() < (1, 6),
56+
reason="shared_db_wrapper needs at least Django 1.6")
57+
class TestSharedDbWrapper(object):
58+
"""Tests for sharing data created with share_db_wrapper, order matters."""
59+
@pytest.fixture(scope='class')
60+
def shared_item(self, request, shared_db_wrapper):
61+
with shared_db_wrapper(request):
62+
return Item.objects.create(name='shared item')
63+
64+
def test_preparing_data(self, shared_item):
65+
type(self)._shared_item_pk = shared_item.pk
66+
67+
def test_accessing_the_same_data(self, db, shared_item):
68+
retrieved_item = Item.objects.get(name='shared item')
69+
assert type(self)._shared_item_pk == retrieved_item.pk
70+
71+
72+
@pytest.mark.skipif(get_django_version() < (1, 6),
73+
reason="shared_db_wrapper needs at least Django 1.6")
74+
def test_shared_db_wrapper_not_leaking(db):
75+
assert not Item.objects.filter(name='shared item').exists()
76+
77+
5478
class TestDatabaseFixtures:
5579
"""Tests for the db and transactional_db fixtures"""
5680

0 commit comments

Comments
 (0)