Skip to content

Commit c2fd0cd

Browse files
committed
Add shared_db_wrapper for creating long-lived db state
1 parent b9eb210 commit c2fd0cd

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']
@@ -195,6 +198,62 @@ def transactional_db(request, _django_db_setup, _django_cursor_wrapper):
195198
return _django_db_fixture_helper(True, request, _django_cursor_wrapper)
196199

197200

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