Skip to content

Handle south migrations #129

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 27, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ NEXT
* Fix admin client with custom user models (#124). Big thanks to Benjamin
Hedrich and Dmitry Dygalo for patch and tests.

* Fix usage of South migrations, which were unconditionally disabled previously
(#22).

2.6.2
-----

Expand Down
6 changes: 4 additions & 2 deletions docs/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,11 @@ This snippet should do the trick (replace ``yourproject.settings`` and make sure


How do South and pytest-django play together?
------------------------------------------------
---------------------------------------------

Django's own syncdb will always be used to create the test database, regardless of whether South is present or not.
pytest-django detects South and applies its monkey-patch, which gets fixed
to handle initial data properly (which South would skip otherwise, because
of a bug).


Does this work with the pytest-xdist plugin?
Expand Down
1 change: 1 addition & 0 deletions generate_configurations.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def requirements(env):
yield 'pytest-xdist==1.10'
yield DJANGO_REQUIREMENTS[env.django_version]
yield 'django-configurations==0.8'
yield 'south==1.0'

if env.settings == 'postgres':
# Django 1.3 does not work with recent psycopg2 versions
Expand Down
40 changes: 35 additions & 5 deletions pytest_django/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ def _django_db_setup(request,
skip_if_no_django()

from .compat import setup_databases, teardown_databases
from django.core import management

# xdist
if hasattr(request.config, 'slaveinput'):
Expand All @@ -38,10 +37,7 @@ def _django_db_setup(request,

monkey_patch_creation_for_db_suffix(db_suffix)

# Disable south's syncdb command
commands = management.get_commands()
if commands['syncdb'] == 'south':
management._commands['syncdb'] = 'django.core'
_handle_south()

with _django_cursor_wrapper:
# Monkey patch Django's setup code to support database re-use
Expand Down Expand Up @@ -93,6 +89,40 @@ def flushdb():
request.addfinalizer(_django_cursor_wrapper.disable)
request.addfinalizer(case._post_teardown)

def _handle_south():
from django.conf import settings
if 'south' in settings.INSTALLED_APPS:
# Handle south.
from django.core import management

try:
# if `south` >= 0.7.1 we can use the test helper
from south.management.commands import patch_for_test_db_setup
except ImportError:
# if `south` < 0.7.1 make sure it's migrations are disabled
management.get_commands()
management._commands['syncdb'] = 'django.core'
else:
# Monkey-patch south.hacks.django_1_0.SkipFlushCommand to load
# initial data.
# Ref: http://south.aeracode.org/ticket/1395#comment:3
import south.hacks.django_1_0
from django.core.management.commands.flush import Command as FlushCommand
class SkipFlushCommand(FlushCommand):
def handle_noargs(self, **options):
# Reinstall the initial_data fixture.
from django.core.management import call_command
# `load_initial_data` got introduces with Django 1.5.
load_initial_data = options.get('load_initial_data', None)
if load_initial_data or load_initial_data is None:
# Reinstall the initial_data fixture.
call_command('loaddata', 'initial_data', **options)
# no-op to avoid calling flush
return
south.hacks.django_1_0.SkipFlushCommand = SkipFlushCommand

patch_for_test_db_setup()

################ User visible fixtures ################


Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ pytest-xdist
tox
wheel
twine
south
15 changes: 14 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
@pytest.fixture(scope='function')
def django_testdir(request, testdir, monkeypatch):
if get_db_engine() in ('mysql', 'postgresql_psycopg2', 'sqlite3'):
# Django requires the production database to exists..
# Django requires the production database to exist.
create_empty_production_database()

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

return testdir


@pytest.fixture
def django_testdir_initial(django_testdir):
"""A django_testdir fixture which provides initial_data."""
django_testdir.makefile('.json', initial_data="""
[{
"pk": 1,
"model": "app.item",
"fields": { "name": "mark_initial_data" }
}]""")

return django_testdir
109 changes: 109 additions & 0 deletions tests/test_db_setup.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import sys
from textwrap import dedent

import pytest

from pytest_django.lazy_django import get_django_version

from .db_helpers import (db_exists, drop_database, mark_database, mark_exists,
skip_if_sqlite_in_memory)

Expand Down Expand Up @@ -191,3 +194,109 @@ def test_a():

result = django_testdir.runpytest('--tb=short', '-vv', '-n1')
result.stdout.fnmatch_lines(['*PASSED*test_a*'])


def test_initial_data(django_testdir_initial):
"""Test that initial data gets loaded."""
django_testdir_initial.create_test_module('''
import pytest

from .app.models import Item

@pytest.mark.django_db
def test_inner_south():
assert [x.name for x in Item.objects.all()] \
== ["mark_initial_data"]
''')

result = django_testdir_initial.runpytest('--tb=short', '-v')
result.stdout.fnmatch_lines(['*test_inner_south*PASSED*'])


# NOTE: South tries to monkey-patch management._commands, which has been
# replaced by lru_cache and would cause an AttributeError.
@pytest.mark.skipif(get_django_version() >= (1, 7),
reason='South fails with Django 1.7.')
@pytest.mark.skipif(sys.version_info[:2] == (3, 4),
reason='South fails on Python 3.4.')
class TestSouth:
"""Test interaction with initial_data and South."""

@pytest.mark.extra_settings(dedent("""
INSTALLED_APPS += [ 'south', ]
SOUTH_TESTS_MIGRATE = True
SOUTH_MIGRATION_MODULES = {
'app': 'app.south_migrations',
}
"""))
def test_initial_data_south(self, django_testdir_initial, settings):
django_testdir_initial.create_test_module('''
import pytest

from .app.models import Item

@pytest.mark.django_db
def test_inner_south():
assert [x.name for x in Item.objects.all()] \
== ["mark_initial_data"]
''')

result = django_testdir_initial.runpytest('--tb=short', '-v')
result.stdout.fnmatch_lines(['*test_inner_south*PASSED*'])

@pytest.mark.extra_settings(dedent("""
INSTALLED_APPS += [ 'south', ]
SOUTH_TESTS_MIGRATE = True
SOUTH_MIGRATION_MODULES = {
'app': 'tpkg.app.south_migrations',
}
"""))
def test_initial_south_migrations(self, django_testdir_initial, settings):
testdir = django_testdir_initial
testdir.create_test_module('''
import pytest

@pytest.mark.django_db
def test_inner_south():
pass
''')

testdir.mkpydir('tpkg/app/south_migrations')
p = testdir.tmpdir.join(
"tpkg/app/south_migrations/0001_initial").new(ext="py")
p.write(dedent("""
from south.v2 import SchemaMigration

class Migration(SchemaMigration):
def forwards(self, orm):
print("mark_south_migration_forwards")
"""), ensure=True)

result = testdir.runpytest('--tb=short', '-v', '-s')
result.stdout.fnmatch_lines(['*mark_south_migration_forwards*'])

@pytest.mark.extra_settings(dedent("""
INSTALLED_APPS += [ 'south', ]
SOUTH_TESTS_MIGRATE = False
SOUTH_MIGRATION_MODULES = {
'app': 'tpkg.app.south_migrations',
}
"""))
def test_south_no_migrations(self, django_testdir_initial, settings):
testdir = django_testdir_initial
testdir.create_test_module('''
import pytest

@pytest.mark.django_db
def test_inner_south():
pass
''')

testdir.mkpydir('tpkg/app/south_migrations')
p = testdir.tmpdir.join(
"tpkg/app/south_migrations/0001_initial").new(ext="py")
p.write('raise Exception("This should not get imported.")',
ensure=True)

result = testdir.runpytest('--tb=short', '-v')
result.stdout.fnmatch_lines(['*test_inner_south*PASSED*'])
Loading