Skip to content

Commit 31e978a

Browse files
committed
Fix south integration with --create-db
This requires to monkey-patch `south.hacks.django_1_0.SkipFlushCommand`, which has a bug that prevents any initial data to be installed. South issue: http://south.aeracode.org/ticket/1395#comment:3 Fixes #22 Django code reference for 1.6, from django-1.6.x/django/db/backends/creation.py(339)create_test_db(): # Report syncdb messages at one level lower than that requested. # This ensures we don't get flooded with messages during testing # (unless you really ask to be flooded) call_command('syncdb', verbosity=max(verbosity - 1, 0), interactive=False, database=self.connection.alias, load_initial_data=False) # We need to then do a flush to ensure that any data installed by # custom SQL has been removed. The only test data should come from # test fixtures, or autogenerated from post_syncdb triggers. # This has the side effect of loading initial data (which was # intentionally skipped in the syncdb). call_command('flush', verbosity=max(verbosity - 1, 0), interactive=False, database=self.connection.alias)
1 parent a3edae8 commit 31e978a

File tree

8 files changed

+270
-3
lines changed

8 files changed

+270
-3
lines changed

Diff for: docs/changelog.rst

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ NEXT
99
* Fix admin client with custom user models (#124). Big thanks to Benjamin
1010
Hedrich and Dmitry Dygalo for patch and tests.
1111

12+
* Fix usage of South migrations, which were unconditionally disabled previously
13+
(#22).
14+
1215
2.6.2
1316
-----
1417

Diff for: docs/faq.rst

+8-2
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,14 @@ This snippet should do the trick (replace ``yourproject.settings`` and make sure
5454
echo "export DJANGO_SETTINGS_MODULE=yourproject.settings" >> $VIRTUAL_ENV/bin/postactivate
5555

5656

57-
How does South and pytest-django play together?
58-
Djangos own syncdb will always be used to create the test database, regardless of wheter South is present or not.
57+
How do South and pytest-django play together?
58+
---------------------------------------------
59+
60+
pytest-django detects South and applies its monkey-patch, which gets fixed
61+
to handle initial data properly (which South would skip otherwise, because
62+
of a bug).
63+
64+
5965
Does this work with the pytest-xdist plugin?
6066
--------------------------------------------
6167

Diff for: generate_configurations.py

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def requirements(env):
6565
yield 'pytest-xdist==1.10'
6666
yield DJANGO_REQUIREMENTS[env.django_version]
6767
yield 'django-configurations==0.8'
68+
yield 'south==1.0'
6869

6970
if env.settings == 'postgres':
7071
# Django 1.3 does not work with recent psycopg2 versions

Diff for: pytest_django/fixtures.py

+36
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ def _django_db_setup(request,
3737

3838
monkey_patch_creation_for_db_suffix(db_suffix)
3939

40+
_handle_south()
41+
4042
with _django_cursor_wrapper:
4143
# Monkey patch Django's setup code to support database re-use
4244
if request.config.getvalue('reuse_db'):
@@ -87,6 +89,40 @@ def flushdb():
8789
request.addfinalizer(_django_cursor_wrapper.disable)
8890
request.addfinalizer(case._post_teardown)
8991

92+
def _handle_south():
93+
from django.conf import settings
94+
if 'south' in settings.INSTALLED_APPS:
95+
# Handle south.
96+
from django.core import management
97+
98+
try:
99+
# if `south` >= 0.7.1 we can use the test helper
100+
from south.management.commands import patch_for_test_db_setup
101+
except ImportError:
102+
# if `south` < 0.7.1 make sure it's migrations are disabled
103+
management.get_commands()
104+
management._commands['syncdb'] = 'django.core'
105+
else:
106+
# Monkey-patch south.hacks.django_1_0.SkipFlushCommand to load
107+
# initial data.
108+
# Ref: http://south.aeracode.org/ticket/1395#comment:3
109+
import south.hacks.django_1_0
110+
from django.core.management.commands.flush import Command as FlushCommand
111+
class SkipFlushCommand(FlushCommand):
112+
def handle_noargs(self, **options):
113+
# Reinstall the initial_data fixture.
114+
from django.core.management import call_command
115+
# `load_initial_data` got introduces with Django 1.5.
116+
load_initial_data = options.get('load_initial_data', None)
117+
if load_initial_data or load_initial_data is None:
118+
# Reinstall the initial_data fixture.
119+
call_command('loaddata', 'initial_data', **options)
120+
# no-op to avoid calling flush
121+
return
122+
south.hacks.django_1_0.SkipFlushCommand = SkipFlushCommand
123+
124+
patch_for_test_db_setup()
125+
90126
################ User visible fixtures ################
91127

92128

Diff for: requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ pytest-xdist
55
tox
66
wheel
77
twine
8+
south

Diff for: tests/conftest.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
@pytest.fixture(scope='function')
2323
def django_testdir(request, testdir, monkeypatch):
2424
if get_db_engine() in ('mysql', 'postgresql_psycopg2', 'sqlite3'):
25-
# Django requires the production database to exists..
25+
# Django requires the production database to exist.
2626
create_empty_production_database()
2727

2828
if hasattr(request.node.cls, 'db_settings'):
@@ -75,3 +75,16 @@ def create_app_file(code, filename):
7575
testdir.create_app_file = create_app_file
7676

7777
return testdir
78+
79+
80+
@pytest.fixture
81+
def django_testdir_initial(django_testdir):
82+
"""A django_testdir fixture which provides initial_data."""
83+
django_testdir.makefile('.json', initial_data="""
84+
[{
85+
"pk": 1,
86+
"model": "app.item",
87+
"fields": { "name": "mark_initial_data" }
88+
}]""")
89+
90+
return django_testdir

Diff for: tests/test_db_setup.py

+109
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import sys
2+
from textwrap import dedent
23

34
import pytest
45

6+
from pytest_django.lazy_django import get_django_version
7+
58
from .db_helpers import (db_exists, drop_database, mark_database, mark_exists,
69
skip_if_sqlite_in_memory)
710

@@ -191,3 +194,109 @@ def test_a():
191194

192195
result = django_testdir.runpytest('--tb=short', '-vv', '-n1')
193196
result.stdout.fnmatch_lines(['*PASSED*test_a*'])
197+
198+
199+
def test_initial_data(django_testdir_initial):
200+
"""Test that initial data gets loaded."""
201+
django_testdir_initial.create_test_module('''
202+
import pytest
203+
204+
from .app.models import Item
205+
206+
@pytest.mark.django_db
207+
def test_inner_south():
208+
assert [x.name for x in Item.objects.all()] \
209+
== ["mark_initial_data"]
210+
''')
211+
212+
result = django_testdir_initial.runpytest('--tb=short', '-v')
213+
result.stdout.fnmatch_lines(['*test_inner_south*PASSED*'])
214+
215+
216+
# NOTE: South tries to monkey-patch management._commands, which has been
217+
# replaced by lru_cache and would cause an AttributeError.
218+
@pytest.mark.skipif(get_django_version() >= (1, 7),
219+
reason='South fails with Django 1.7.')
220+
@pytest.mark.skipif(sys.version_info[:2] == (3, 4),
221+
reason='South fails on Python 3.4.')
222+
class TestSouth:
223+
"""Test interaction with initial_data and South."""
224+
225+
@pytest.mark.extra_settings(dedent("""
226+
INSTALLED_APPS += [ 'south', ]
227+
SOUTH_TESTS_MIGRATE = True
228+
SOUTH_MIGRATION_MODULES = {
229+
'app': 'app.south_migrations',
230+
}
231+
"""))
232+
def test_initial_data_south(self, django_testdir_initial, settings):
233+
django_testdir_initial.create_test_module('''
234+
import pytest
235+
236+
from .app.models import Item
237+
238+
@pytest.mark.django_db
239+
def test_inner_south():
240+
assert [x.name for x in Item.objects.all()] \
241+
== ["mark_initial_data"]
242+
''')
243+
244+
result = django_testdir_initial.runpytest('--tb=short', '-v')
245+
result.stdout.fnmatch_lines(['*test_inner_south*PASSED*'])
246+
247+
@pytest.mark.extra_settings(dedent("""
248+
INSTALLED_APPS += [ 'south', ]
249+
SOUTH_TESTS_MIGRATE = True
250+
SOUTH_MIGRATION_MODULES = {
251+
'app': 'tpkg.app.south_migrations',
252+
}
253+
"""))
254+
def test_initial_south_migrations(self, django_testdir_initial, settings):
255+
testdir = django_testdir_initial
256+
testdir.create_test_module('''
257+
import pytest
258+
259+
@pytest.mark.django_db
260+
def test_inner_south():
261+
pass
262+
''')
263+
264+
testdir.mkpydir('tpkg/app/south_migrations')
265+
p = testdir.tmpdir.join(
266+
"tpkg/app/south_migrations/0001_initial").new(ext="py")
267+
p.write(dedent("""
268+
from south.v2 import SchemaMigration
269+
270+
class Migration(SchemaMigration):
271+
def forwards(self, orm):
272+
print("mark_south_migration_forwards")
273+
"""), ensure=True)
274+
275+
result = testdir.runpytest('--tb=short', '-v', '-s')
276+
result.stdout.fnmatch_lines(['*mark_south_migration_forwards*'])
277+
278+
@pytest.mark.extra_settings(dedent("""
279+
INSTALLED_APPS += [ 'south', ]
280+
SOUTH_TESTS_MIGRATE = False
281+
SOUTH_MIGRATION_MODULES = {
282+
'app': 'tpkg.app.south_migrations',
283+
}
284+
"""))
285+
def test_south_no_migrations(self, django_testdir_initial, settings):
286+
testdir = django_testdir_initial
287+
testdir.create_test_module('''
288+
import pytest
289+
290+
@pytest.mark.django_db
291+
def test_inner_south():
292+
pass
293+
''')
294+
295+
testdir.mkpydir('tpkg/app/south_migrations')
296+
p = testdir.tmpdir.join(
297+
"tpkg/app/south_migrations/0001_initial").new(ext="py")
298+
p.write('raise Exception("This should not get imported.")',
299+
ensure=True)
300+
301+
result = testdir.runpytest('--tb=short', '-v')
302+
result.stdout.fnmatch_lines(['*test_inner_south*PASSED*'])

0 commit comments

Comments
 (0)