From 0c2d82b5be933680371efe47c638f860f4b359e7 Mon Sep 17 00:00:00 2001 From: Brandon Savage Date: Thu, 20 Sep 2012 14:23:54 -0400 Subject: [PATCH] adding south --- .../South-0.7.6-py2.7.egg-info/PKG-INFO | 19 + .../South-0.7.6-py2.7.egg-info/SOURCES.txt | 121 ++ .../dependency_links.txt | 1 + .../installed-files.txt | 233 ++++ .../South-0.7.6-py2.7.egg-info/top_level.txt | 1 + lib/python/south/__init__.py | 9 + lib/python/south/__init__.pyc | Bin 0 -> 360 bytes lib/python/south/creator/__init__.py | 5 + lib/python/south/creator/__init__.pyc | Bin 0 -> 356 bytes lib/python/south/creator/actions.py | 540 ++++++++ lib/python/south/creator/actions.pyc | Bin 0 -> 23200 bytes lib/python/south/creator/changes.py | 489 +++++++ lib/python/south/creator/changes.pyc | Bin 0 -> 13247 bytes lib/python/south/creator/freezer.py | 190 +++ lib/python/south/creator/freezer.pyc | Bin 0 -> 6986 bytes lib/python/south/db/__init__.py | 81 ++ lib/python/south/db/__init__.pyc | Bin 0 -> 2095 bytes lib/python/south/db/firebird.py | 322 +++++ lib/python/south/db/firebird.pyc | Bin 0 -> 9224 bytes lib/python/south/db/generic.py | 1139 +++++++++++++++++ lib/python/south/db/generic.pyc | Bin 0 -> 40648 bytes lib/python/south/db/mysql.py | 280 ++++ lib/python/south/db/mysql.pyc | Bin 0 -> 10703 bytes lib/python/south/db/oracle.py | 299 +++++ lib/python/south/db/oracle.pyc | Bin 0 -> 11096 bytes lib/python/south/db/postgresql_psycopg2.py | 89 ++ lib/python/south/db/postgresql_psycopg2.pyc | Bin 0 -> 3706 bytes lib/python/south/db/sql_server/__init__.py | 0 lib/python/south/db/sql_server/__init__.pyc | Bin 0 -> 161 bytes lib/python/south/db/sql_server/pyodbc.py | 434 +++++++ lib/python/south/db/sql_server/pyodbc.pyc | Bin 0 -> 17251 bytes lib/python/south/db/sqlite3.py | 252 ++++ lib/python/south/db/sqlite3.pyc | Bin 0 -> 9158 bytes lib/python/south/exceptions.py | 151 +++ lib/python/south/exceptions.pyc | Bin 0 -> 10501 bytes lib/python/south/hacks/__init__.py | 10 + lib/python/south/hacks/__init__.pyc | Bin 0 -> 439 bytes lib/python/south/hacks/django_1_0.py | 107 ++ lib/python/south/hacks/django_1_0.pyc | Bin 0 -> 4798 bytes .../south/introspection_plugins/__init__.py | 10 + .../south/introspection_plugins/__init__.pyc | Bin 0 -> 498 bytes .../annoying_autoonetoone.py | 11 + .../annoying_autoonetoone.pyc | Bin 0 -> 551 bytes .../introspection_plugins/django_audit_log.py | 30 + .../django_audit_log.pyc | Bin 0 -> 886 bytes .../django_objectpermissions.py | 16 + .../django_objectpermissions.pyc | Bin 0 -> 764 bytes .../introspection_plugins/django_tagging.py | 24 + .../introspection_plugins/django_tagging.pyc | Bin 0 -> 764 bytes .../introspection_plugins/django_taggit.py | 14 + .../introspection_plugins/django_taggit.pyc | Bin 0 -> 582 bytes .../introspection_plugins/django_timezones.py | 21 + .../django_timezones.pyc | Bin 0 -> 658 bytes .../south/introspection_plugins/geodjango.py | 45 + .../south/introspection_plugins/geodjango.pyc | Bin 0 -> 1008 bytes lib/python/south/logger.py | 38 + lib/python/south/logger.pyc | Bin 0 -> 1971 bytes lib/python/south/management/__init__.py | 0 lib/python/south/management/__init__.pyc | Bin 0 -> 158 bytes .../south/management/commands/__init__.py | 40 + .../south/management/commands/__init__.pyc | Bin 0 -> 1464 bytes .../management/commands/convert_to_south.py | 93 ++ .../management/commands/convert_to_south.pyc | Bin 0 -> 3581 bytes .../management/commands/datamigration.py | 124 ++ .../management/commands/datamigration.pyc | Bin 0 -> 4643 bytes .../management/commands/graphmigrations.py | 61 + .../management/commands/graphmigrations.pyc | Bin 0 -> 2158 bytes .../south/management/commands/migrate.py | 260 ++++ .../south/management/commands/migrate.pyc | Bin 0 -> 8865 bytes .../management/commands/migrationcheck.py | 67 + .../management/commands/migrationcheck.pyc | Bin 0 -> 2704 bytes .../management/commands/schemamigration.py | 215 ++++ .../management/commands/schemamigration.pyc | Bin 0 -> 8190 bytes .../management/commands/startmigration.py | 31 + .../management/commands/startmigration.pyc | Bin 0 -> 2461 bytes .../south/management/commands/syncdb.py | 111 ++ .../south/management/commands/syncdb.pyc | Bin 0 -> 4063 bytes lib/python/south/management/commands/test.py | 8 + lib/python/south/management/commands/test.pyc | Bin 0 -> 806 bytes .../south/management/commands/testserver.py | 8 + .../south/management/commands/testserver.pyc | Bin 0 -> 830 bytes lib/python/south/migration/__init__.py | 228 ++++ lib/python/south/migration/__init__.pyc | Bin 0 -> 7424 bytes lib/python/south/migration/base.py | 439 +++++++ lib/python/south/migration/base.pyc | Bin 0 -> 19326 bytes lib/python/south/migration/migrators.py | 360 ++++++ lib/python/south/migration/migrators.pyc | Bin 0 -> 15677 bytes lib/python/south/migration/utils.py | 83 ++ lib/python/south/migration/utils.pyc | Bin 0 -> 4340 bytes lib/python/south/models.py | 37 + lib/python/south/models.pyc | Bin 0 -> 1767 bytes lib/python/south/modelsinspector.py | 462 +++++++ lib/python/south/modelsinspector.pyc | Bin 0 -> 13046 bytes lib/python/south/orm.py | 400 ++++++ lib/python/south/orm.pyc | Bin 0 -> 11957 bytes lib/python/south/signals.py | 24 + lib/python/south/signals.pyc | Bin 0 -> 483 bytes lib/python/south/tests/__init__.py | 89 ++ lib/python/south/tests/__init__.pyc | Bin 0 -> 3271 bytes lib/python/south/tests/autodetection.py | 353 +++++ lib/python/south/tests/autodetection.pyc | Bin 0 -> 9138 bytes lib/python/south/tests/brokenapp/__init__.py | 0 lib/python/south/tests/brokenapp/__init__.pyc | Bin 0 -> 163 bytes .../migrations/0001_depends_on_unmigrated.py | 13 + .../migrations/0001_depends_on_unmigrated.pyc | Bin 0 -> 1044 bytes .../migrations/0002_depends_on_unknown.py | 13 + .../migrations/0002_depends_on_unknown.pyc | Bin 0 -> 1025 bytes .../migrations/0003_depends_on_higher.py | 13 + .../migrations/0003_depends_on_higher.pyc | Bin 0 -> 1022 bytes .../tests/brokenapp/migrations/0004_higher.py | 11 + .../brokenapp/migrations/0004_higher.pyc | Bin 0 -> 907 bytes .../tests/brokenapp/migrations/__init__.py | 0 .../tests/brokenapp/migrations/__init__.pyc | Bin 0 -> 174 bytes lib/python/south/tests/brokenapp/models.py | 55 + lib/python/south/tests/brokenapp/models.pyc | Bin 0 -> 2345 bytes lib/python/south/tests/circular_a/__init__.py | 0 .../south/tests/circular_a/__init__.pyc | Bin 0 -> 164 bytes .../tests/circular_a/migrations/0001_first.py | 13 + .../circular_a/migrations/0001_first.pyc | Bin 0 -> 978 bytes .../tests/circular_a/migrations/__init__.py | 0 .../tests/circular_a/migrations/__init__.pyc | Bin 0 -> 175 bytes lib/python/south/tests/circular_a/models.py | 0 lib/python/south/tests/circular_a/models.pyc | Bin 0 -> 162 bytes lib/python/south/tests/circular_b/__init__.py | 0 .../south/tests/circular_b/__init__.pyc | Bin 0 -> 164 bytes .../tests/circular_b/migrations/0001_first.py | 13 + .../circular_b/migrations/0001_first.pyc | Bin 0 -> 978 bytes .../tests/circular_b/migrations/__init__.py | 0 .../tests/circular_b/migrations/__init__.pyc | Bin 0 -> 175 bytes lib/python/south/tests/circular_b/models.py | 0 lib/python/south/tests/circular_b/models.pyc | Bin 0 -> 162 bytes lib/python/south/tests/db.py | 857 +++++++++++++ lib/python/south/tests/db.pyc | Bin 0 -> 29603 bytes lib/python/south/tests/db_mysql.py | 165 +++ lib/python/south/tests/db_mysql.pyc | Bin 0 -> 6413 bytes lib/python/south/tests/deps_a/__init__.py | 0 lib/python/south/tests/deps_a/__init__.pyc | Bin 0 -> 160 bytes .../south/tests/deps_a/migrations/0001_a.py | 11 + .../south/tests/deps_a/migrations/0001_a.pyc | Bin 0 -> 875 bytes .../south/tests/deps_a/migrations/0002_a.py | 11 + .../south/tests/deps_a/migrations/0002_a.pyc | Bin 0 -> 875 bytes .../south/tests/deps_a/migrations/0003_a.py | 11 + .../south/tests/deps_a/migrations/0003_a.pyc | Bin 0 -> 875 bytes .../south/tests/deps_a/migrations/0004_a.py | 13 + .../south/tests/deps_a/migrations/0004_a.pyc | Bin 0 -> 938 bytes .../south/tests/deps_a/migrations/0005_a.py | 11 + .../south/tests/deps_a/migrations/0005_a.pyc | Bin 0 -> 875 bytes .../south/tests/deps_a/migrations/__init__.py | 0 .../tests/deps_a/migrations/__init__.pyc | Bin 0 -> 171 bytes lib/python/south/tests/deps_a/models.py | 0 lib/python/south/tests/deps_a/models.pyc | Bin 0 -> 158 bytes lib/python/south/tests/deps_b/__init__.py | 0 lib/python/south/tests/deps_b/__init__.pyc | Bin 0 -> 160 bytes .../south/tests/deps_b/migrations/0001_b.py | 11 + .../south/tests/deps_b/migrations/0001_b.pyc | Bin 0 -> 875 bytes .../south/tests/deps_b/migrations/0002_b.py | 13 + .../south/tests/deps_b/migrations/0002_b.pyc | Bin 0 -> 938 bytes .../south/tests/deps_b/migrations/0003_b.py | 13 + .../south/tests/deps_b/migrations/0003_b.pyc | Bin 0 -> 938 bytes .../south/tests/deps_b/migrations/0004_b.py | 11 + .../south/tests/deps_b/migrations/0004_b.pyc | Bin 0 -> 875 bytes .../south/tests/deps_b/migrations/0005_b.py | 11 + .../south/tests/deps_b/migrations/0005_b.pyc | Bin 0 -> 875 bytes .../south/tests/deps_b/migrations/__init__.py | 0 .../tests/deps_b/migrations/__init__.pyc | Bin 0 -> 171 bytes lib/python/south/tests/deps_b/models.py | 0 lib/python/south/tests/deps_b/models.pyc | Bin 0 -> 158 bytes lib/python/south/tests/deps_c/__init__.py | 0 lib/python/south/tests/deps_c/__init__.pyc | Bin 0 -> 160 bytes .../south/tests/deps_c/migrations/0001_c.py | 11 + .../south/tests/deps_c/migrations/0001_c.pyc | Bin 0 -> 875 bytes .../south/tests/deps_c/migrations/0002_c.py | 11 + .../south/tests/deps_c/migrations/0002_c.pyc | Bin 0 -> 875 bytes .../south/tests/deps_c/migrations/0003_c.py | 11 + .../south/tests/deps_c/migrations/0003_c.pyc | Bin 0 -> 875 bytes .../south/tests/deps_c/migrations/0004_c.py | 11 + .../south/tests/deps_c/migrations/0004_c.pyc | Bin 0 -> 875 bytes .../south/tests/deps_c/migrations/0005_c.py | 13 + .../south/tests/deps_c/migrations/0005_c.pyc | Bin 0 -> 938 bytes .../south/tests/deps_c/migrations/__init__.py | 0 .../tests/deps_c/migrations/__init__.pyc | Bin 0 -> 171 bytes lib/python/south/tests/deps_c/models.py | 0 lib/python/south/tests/deps_c/models.pyc | Bin 0 -> 158 bytes lib/python/south/tests/emptyapp/__init__.py | 0 lib/python/south/tests/emptyapp/__init__.pyc | Bin 0 -> 162 bytes .../tests/emptyapp/migrations/__init__.py | 0 .../tests/emptyapp/migrations/__init__.pyc | Bin 0 -> 173 bytes lib/python/south/tests/emptyapp/models.py | 0 lib/python/south/tests/emptyapp/models.pyc | Bin 0 -> 160 bytes lib/python/south/tests/fakeapp/__init__.py | 0 lib/python/south/tests/fakeapp/__init__.pyc | Bin 0 -> 161 bytes .../tests/fakeapp/migrations/0001_spam.py | 17 + .../tests/fakeapp/migrations/0001_spam.pyc | Bin 0 -> 1271 bytes .../tests/fakeapp/migrations/0002_eggs.py | 20 + .../tests/fakeapp/migrations/0002_eggs.pyc | Bin 0 -> 1429 bytes .../fakeapp/migrations/0003_alter_spam.py | 18 + .../fakeapp/migrations/0003_alter_spam.pyc | Bin 0 -> 1257 bytes .../tests/fakeapp/migrations/__init__.py | 0 .../tests/fakeapp/migrations/__init__.pyc | Bin 0 -> 172 bytes lib/python/south/tests/fakeapp/models.py | 111 ++ lib/python/south/tests/fakeapp/models.pyc | Bin 0 -> 5748 bytes lib/python/south/tests/freezer.py | 15 + lib/python/south/tests/freezer.pyc | Bin 0 -> 1062 bytes lib/python/south/tests/inspector.py | 109 ++ lib/python/south/tests/inspector.pyc | Bin 0 -> 2796 bytes lib/python/south/tests/logger.py | 82 ++ lib/python/south/tests/logger.pyc | Bin 0 -> 3326 bytes lib/python/south/tests/logic.py | 898 +++++++++++++ lib/python/south/tests/logic.pyc | Bin 0 -> 25670 bytes .../south/tests/non_managed/__init__.py | 0 .../south/tests/non_managed/__init__.pyc | Bin 0 -> 165 bytes .../tests/non_managed/migrations/__init__.py | 0 .../tests/non_managed/migrations/__init__.pyc | Bin 0 -> 176 bytes lib/python/south/tests/non_managed/models.py | 16 + lib/python/south/tests/non_managed/models.pyc | Bin 0 -> 952 bytes .../south/tests/otherfakeapp/__init__.py | 0 .../south/tests/otherfakeapp/__init__.pyc | Bin 0 -> 166 bytes .../otherfakeapp/migrations/0001_first.py | 15 + .../otherfakeapp/migrations/0001_first.pyc | Bin 0 -> 999 bytes .../otherfakeapp/migrations/0002_second.py | 11 + .../otherfakeapp/migrations/0002_second.pyc | Bin 0 -> 919 bytes .../otherfakeapp/migrations/0003_third.py | 14 + .../otherfakeapp/migrations/0003_third.pyc | Bin 0 -> 1005 bytes .../tests/otherfakeapp/migrations/__init__.py | 0 .../otherfakeapp/migrations/__init__.pyc | Bin 0 -> 177 bytes lib/python/south/tests/otherfakeapp/models.py | 1 + .../south/tests/otherfakeapp/models.pyc | Bin 0 -> 164 bytes lib/python/south/utils/__init__.py | 73 ++ lib/python/south/utils/__init__.pyc | Bin 0 -> 2889 bytes lib/python/south/utils/datetime_utils.py | 28 + lib/python/south/utils/datetime_utils.pyc | Bin 0 -> 1495 bytes lib/python/south/v2.py | 19 + lib/python/south/v2.pyc | Bin 0 -> 1198 bytes 233 files changed, 11247 insertions(+) create mode 100644 lib/python/South-0.7.6-py2.7.egg-info/PKG-INFO create mode 100644 lib/python/South-0.7.6-py2.7.egg-info/SOURCES.txt create mode 100644 lib/python/South-0.7.6-py2.7.egg-info/dependency_links.txt create mode 100644 lib/python/South-0.7.6-py2.7.egg-info/installed-files.txt create mode 100644 lib/python/South-0.7.6-py2.7.egg-info/top_level.txt create mode 100644 lib/python/south/__init__.py create mode 100644 lib/python/south/__init__.pyc create mode 100644 lib/python/south/creator/__init__.py create mode 100644 lib/python/south/creator/__init__.pyc create mode 100644 lib/python/south/creator/actions.py create mode 100644 lib/python/south/creator/actions.pyc create mode 100644 lib/python/south/creator/changes.py create mode 100644 lib/python/south/creator/changes.pyc create mode 100644 lib/python/south/creator/freezer.py create mode 100644 lib/python/south/creator/freezer.pyc create mode 100644 lib/python/south/db/__init__.py create mode 100644 lib/python/south/db/__init__.pyc create mode 100644 lib/python/south/db/firebird.py create mode 100644 lib/python/south/db/firebird.pyc create mode 100644 lib/python/south/db/generic.py create mode 100644 lib/python/south/db/generic.pyc create mode 100644 lib/python/south/db/mysql.py create mode 100644 lib/python/south/db/mysql.pyc create mode 100644 lib/python/south/db/oracle.py create mode 100644 lib/python/south/db/oracle.pyc create mode 100644 lib/python/south/db/postgresql_psycopg2.py create mode 100644 lib/python/south/db/postgresql_psycopg2.pyc create mode 100644 lib/python/south/db/sql_server/__init__.py create mode 100644 lib/python/south/db/sql_server/__init__.pyc create mode 100644 lib/python/south/db/sql_server/pyodbc.py create mode 100644 lib/python/south/db/sql_server/pyodbc.pyc create mode 100644 lib/python/south/db/sqlite3.py create mode 100644 lib/python/south/db/sqlite3.pyc create mode 100644 lib/python/south/exceptions.py create mode 100644 lib/python/south/exceptions.pyc create mode 100644 lib/python/south/hacks/__init__.py create mode 100644 lib/python/south/hacks/__init__.pyc create mode 100644 lib/python/south/hacks/django_1_0.py create mode 100644 lib/python/south/hacks/django_1_0.pyc create mode 100644 lib/python/south/introspection_plugins/__init__.py create mode 100644 lib/python/south/introspection_plugins/__init__.pyc create mode 100644 lib/python/south/introspection_plugins/annoying_autoonetoone.py create mode 100644 lib/python/south/introspection_plugins/annoying_autoonetoone.pyc create mode 100644 lib/python/south/introspection_plugins/django_audit_log.py create mode 100644 lib/python/south/introspection_plugins/django_audit_log.pyc create mode 100644 lib/python/south/introspection_plugins/django_objectpermissions.py create mode 100644 lib/python/south/introspection_plugins/django_objectpermissions.pyc create mode 100644 lib/python/south/introspection_plugins/django_tagging.py create mode 100644 lib/python/south/introspection_plugins/django_tagging.pyc create mode 100644 lib/python/south/introspection_plugins/django_taggit.py create mode 100644 lib/python/south/introspection_plugins/django_taggit.pyc create mode 100644 lib/python/south/introspection_plugins/django_timezones.py create mode 100644 lib/python/south/introspection_plugins/django_timezones.pyc create mode 100644 lib/python/south/introspection_plugins/geodjango.py create mode 100644 lib/python/south/introspection_plugins/geodjango.pyc create mode 100644 lib/python/south/logger.py create mode 100644 lib/python/south/logger.pyc create mode 100644 lib/python/south/management/__init__.py create mode 100644 lib/python/south/management/__init__.pyc create mode 100644 lib/python/south/management/commands/__init__.py create mode 100644 lib/python/south/management/commands/__init__.pyc create mode 100644 lib/python/south/management/commands/convert_to_south.py create mode 100644 lib/python/south/management/commands/convert_to_south.pyc create mode 100644 lib/python/south/management/commands/datamigration.py create mode 100644 lib/python/south/management/commands/datamigration.pyc create mode 100644 lib/python/south/management/commands/graphmigrations.py create mode 100644 lib/python/south/management/commands/graphmigrations.pyc create mode 100644 lib/python/south/management/commands/migrate.py create mode 100644 lib/python/south/management/commands/migrate.pyc create mode 100644 lib/python/south/management/commands/migrationcheck.py create mode 100644 lib/python/south/management/commands/migrationcheck.pyc create mode 100644 lib/python/south/management/commands/schemamigration.py create mode 100644 lib/python/south/management/commands/schemamigration.pyc create mode 100644 lib/python/south/management/commands/startmigration.py create mode 100644 lib/python/south/management/commands/startmigration.pyc create mode 100644 lib/python/south/management/commands/syncdb.py create mode 100644 lib/python/south/management/commands/syncdb.pyc create mode 100644 lib/python/south/management/commands/test.py create mode 100644 lib/python/south/management/commands/test.pyc create mode 100644 lib/python/south/management/commands/testserver.py create mode 100644 lib/python/south/management/commands/testserver.pyc create mode 100644 lib/python/south/migration/__init__.py create mode 100644 lib/python/south/migration/__init__.pyc create mode 100644 lib/python/south/migration/base.py create mode 100644 lib/python/south/migration/base.pyc create mode 100644 lib/python/south/migration/migrators.py create mode 100644 lib/python/south/migration/migrators.pyc create mode 100644 lib/python/south/migration/utils.py create mode 100644 lib/python/south/migration/utils.pyc create mode 100644 lib/python/south/models.py create mode 100644 lib/python/south/models.pyc create mode 100644 lib/python/south/modelsinspector.py create mode 100644 lib/python/south/modelsinspector.pyc create mode 100644 lib/python/south/orm.py create mode 100644 lib/python/south/orm.pyc create mode 100644 lib/python/south/signals.py create mode 100644 lib/python/south/signals.pyc create mode 100644 lib/python/south/tests/__init__.py create mode 100644 lib/python/south/tests/__init__.pyc create mode 100644 lib/python/south/tests/autodetection.py create mode 100644 lib/python/south/tests/autodetection.pyc create mode 100644 lib/python/south/tests/brokenapp/__init__.py create mode 100644 lib/python/south/tests/brokenapp/__init__.pyc create mode 100644 lib/python/south/tests/brokenapp/migrations/0001_depends_on_unmigrated.py create mode 100644 lib/python/south/tests/brokenapp/migrations/0001_depends_on_unmigrated.pyc create mode 100644 lib/python/south/tests/brokenapp/migrations/0002_depends_on_unknown.py create mode 100644 lib/python/south/tests/brokenapp/migrations/0002_depends_on_unknown.pyc create mode 100644 lib/python/south/tests/brokenapp/migrations/0003_depends_on_higher.py create mode 100644 lib/python/south/tests/brokenapp/migrations/0003_depends_on_higher.pyc create mode 100644 lib/python/south/tests/brokenapp/migrations/0004_higher.py create mode 100644 lib/python/south/tests/brokenapp/migrations/0004_higher.pyc create mode 100644 lib/python/south/tests/brokenapp/migrations/__init__.py create mode 100644 lib/python/south/tests/brokenapp/migrations/__init__.pyc create mode 100644 lib/python/south/tests/brokenapp/models.py create mode 100644 lib/python/south/tests/brokenapp/models.pyc create mode 100644 lib/python/south/tests/circular_a/__init__.py create mode 100644 lib/python/south/tests/circular_a/__init__.pyc create mode 100644 lib/python/south/tests/circular_a/migrations/0001_first.py create mode 100644 lib/python/south/tests/circular_a/migrations/0001_first.pyc create mode 100644 lib/python/south/tests/circular_a/migrations/__init__.py create mode 100644 lib/python/south/tests/circular_a/migrations/__init__.pyc create mode 100644 lib/python/south/tests/circular_a/models.py create mode 100644 lib/python/south/tests/circular_a/models.pyc create mode 100644 lib/python/south/tests/circular_b/__init__.py create mode 100644 lib/python/south/tests/circular_b/__init__.pyc create mode 100644 lib/python/south/tests/circular_b/migrations/0001_first.py create mode 100644 lib/python/south/tests/circular_b/migrations/0001_first.pyc create mode 100644 lib/python/south/tests/circular_b/migrations/__init__.py create mode 100644 lib/python/south/tests/circular_b/migrations/__init__.pyc create mode 100644 lib/python/south/tests/circular_b/models.py create mode 100644 lib/python/south/tests/circular_b/models.pyc create mode 100644 lib/python/south/tests/db.py create mode 100644 lib/python/south/tests/db.pyc create mode 100644 lib/python/south/tests/db_mysql.py create mode 100644 lib/python/south/tests/db_mysql.pyc create mode 100644 lib/python/south/tests/deps_a/__init__.py create mode 100644 lib/python/south/tests/deps_a/__init__.pyc create mode 100644 lib/python/south/tests/deps_a/migrations/0001_a.py create mode 100644 lib/python/south/tests/deps_a/migrations/0001_a.pyc create mode 100644 lib/python/south/tests/deps_a/migrations/0002_a.py create mode 100644 lib/python/south/tests/deps_a/migrations/0002_a.pyc create mode 100644 lib/python/south/tests/deps_a/migrations/0003_a.py create mode 100644 lib/python/south/tests/deps_a/migrations/0003_a.pyc create mode 100644 lib/python/south/tests/deps_a/migrations/0004_a.py create mode 100644 lib/python/south/tests/deps_a/migrations/0004_a.pyc create mode 100644 lib/python/south/tests/deps_a/migrations/0005_a.py create mode 100644 lib/python/south/tests/deps_a/migrations/0005_a.pyc create mode 100644 lib/python/south/tests/deps_a/migrations/__init__.py create mode 100644 lib/python/south/tests/deps_a/migrations/__init__.pyc create mode 100644 lib/python/south/tests/deps_a/models.py create mode 100644 lib/python/south/tests/deps_a/models.pyc create mode 100644 lib/python/south/tests/deps_b/__init__.py create mode 100644 lib/python/south/tests/deps_b/__init__.pyc create mode 100644 lib/python/south/tests/deps_b/migrations/0001_b.py create mode 100644 lib/python/south/tests/deps_b/migrations/0001_b.pyc create mode 100644 lib/python/south/tests/deps_b/migrations/0002_b.py create mode 100644 lib/python/south/tests/deps_b/migrations/0002_b.pyc create mode 100644 lib/python/south/tests/deps_b/migrations/0003_b.py create mode 100644 lib/python/south/tests/deps_b/migrations/0003_b.pyc create mode 100644 lib/python/south/tests/deps_b/migrations/0004_b.py create mode 100644 lib/python/south/tests/deps_b/migrations/0004_b.pyc create mode 100644 lib/python/south/tests/deps_b/migrations/0005_b.py create mode 100644 lib/python/south/tests/deps_b/migrations/0005_b.pyc create mode 100644 lib/python/south/tests/deps_b/migrations/__init__.py create mode 100644 lib/python/south/tests/deps_b/migrations/__init__.pyc create mode 100644 lib/python/south/tests/deps_b/models.py create mode 100644 lib/python/south/tests/deps_b/models.pyc create mode 100644 lib/python/south/tests/deps_c/__init__.py create mode 100644 lib/python/south/tests/deps_c/__init__.pyc create mode 100644 lib/python/south/tests/deps_c/migrations/0001_c.py create mode 100644 lib/python/south/tests/deps_c/migrations/0001_c.pyc create mode 100644 lib/python/south/tests/deps_c/migrations/0002_c.py create mode 100644 lib/python/south/tests/deps_c/migrations/0002_c.pyc create mode 100644 lib/python/south/tests/deps_c/migrations/0003_c.py create mode 100644 lib/python/south/tests/deps_c/migrations/0003_c.pyc create mode 100644 lib/python/south/tests/deps_c/migrations/0004_c.py create mode 100644 lib/python/south/tests/deps_c/migrations/0004_c.pyc create mode 100644 lib/python/south/tests/deps_c/migrations/0005_c.py create mode 100644 lib/python/south/tests/deps_c/migrations/0005_c.pyc create mode 100644 lib/python/south/tests/deps_c/migrations/__init__.py create mode 100644 lib/python/south/tests/deps_c/migrations/__init__.pyc create mode 100644 lib/python/south/tests/deps_c/models.py create mode 100644 lib/python/south/tests/deps_c/models.pyc create mode 100644 lib/python/south/tests/emptyapp/__init__.py create mode 100644 lib/python/south/tests/emptyapp/__init__.pyc create mode 100644 lib/python/south/tests/emptyapp/migrations/__init__.py create mode 100644 lib/python/south/tests/emptyapp/migrations/__init__.pyc create mode 100644 lib/python/south/tests/emptyapp/models.py create mode 100644 lib/python/south/tests/emptyapp/models.pyc create mode 100644 lib/python/south/tests/fakeapp/__init__.py create mode 100644 lib/python/south/tests/fakeapp/__init__.pyc create mode 100644 lib/python/south/tests/fakeapp/migrations/0001_spam.py create mode 100644 lib/python/south/tests/fakeapp/migrations/0001_spam.pyc create mode 100644 lib/python/south/tests/fakeapp/migrations/0002_eggs.py create mode 100644 lib/python/south/tests/fakeapp/migrations/0002_eggs.pyc create mode 100644 lib/python/south/tests/fakeapp/migrations/0003_alter_spam.py create mode 100644 lib/python/south/tests/fakeapp/migrations/0003_alter_spam.pyc create mode 100644 lib/python/south/tests/fakeapp/migrations/__init__.py create mode 100644 lib/python/south/tests/fakeapp/migrations/__init__.pyc create mode 100644 lib/python/south/tests/fakeapp/models.py create mode 100644 lib/python/south/tests/fakeapp/models.pyc create mode 100644 lib/python/south/tests/freezer.py create mode 100644 lib/python/south/tests/freezer.pyc create mode 100644 lib/python/south/tests/inspector.py create mode 100644 lib/python/south/tests/inspector.pyc create mode 100644 lib/python/south/tests/logger.py create mode 100644 lib/python/south/tests/logger.pyc create mode 100644 lib/python/south/tests/logic.py create mode 100644 lib/python/south/tests/logic.pyc create mode 100644 lib/python/south/tests/non_managed/__init__.py create mode 100644 lib/python/south/tests/non_managed/__init__.pyc create mode 100644 lib/python/south/tests/non_managed/migrations/__init__.py create mode 100644 lib/python/south/tests/non_managed/migrations/__init__.pyc create mode 100644 lib/python/south/tests/non_managed/models.py create mode 100644 lib/python/south/tests/non_managed/models.pyc create mode 100644 lib/python/south/tests/otherfakeapp/__init__.py create mode 100644 lib/python/south/tests/otherfakeapp/__init__.pyc create mode 100644 lib/python/south/tests/otherfakeapp/migrations/0001_first.py create mode 100644 lib/python/south/tests/otherfakeapp/migrations/0001_first.pyc create mode 100644 lib/python/south/tests/otherfakeapp/migrations/0002_second.py create mode 100644 lib/python/south/tests/otherfakeapp/migrations/0002_second.pyc create mode 100644 lib/python/south/tests/otherfakeapp/migrations/0003_third.py create mode 100644 lib/python/south/tests/otherfakeapp/migrations/0003_third.pyc create mode 100644 lib/python/south/tests/otherfakeapp/migrations/__init__.py create mode 100644 lib/python/south/tests/otherfakeapp/migrations/__init__.pyc create mode 100644 lib/python/south/tests/otherfakeapp/models.py create mode 100644 lib/python/south/tests/otherfakeapp/models.pyc create mode 100644 lib/python/south/utils/__init__.py create mode 100644 lib/python/south/utils/__init__.pyc create mode 100644 lib/python/south/utils/datetime_utils.py create mode 100644 lib/python/south/utils/datetime_utils.pyc create mode 100644 lib/python/south/v2.py create mode 100644 lib/python/south/v2.pyc diff --git a/lib/python/South-0.7.6-py2.7.egg-info/PKG-INFO b/lib/python/South-0.7.6-py2.7.egg-info/PKG-INFO new file mode 100644 index 0000000..0984da4 --- /dev/null +++ b/lib/python/South-0.7.6-py2.7.egg-info/PKG-INFO @@ -0,0 +1,19 @@ +Metadata-Version: 1.0 +Name: South +Version: 0.7.6 +Summary: South: Migrations for Django +Home-page: http://south.aeracode.org/ +Author: Andrew Godwin & Andy McCurdy +Author-email: south@aeracode.org +License: UNKNOWN +Download-URL: http://south.aeracode.org/wiki/Download +Description: South is an intelligent database migrations library for the Django web framework. It is database-independent and DVCS-friendly, as well as a whole host of other features. +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Framework :: Django +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: System Administrators +Classifier: Intended Audience :: System Administrators +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Operating System :: OS Independent +Classifier: Topic :: Software Development diff --git a/lib/python/South-0.7.6-py2.7.egg-info/SOURCES.txt b/lib/python/South-0.7.6-py2.7.egg-info/SOURCES.txt new file mode 100644 index 0000000..110ca8c --- /dev/null +++ b/lib/python/South-0.7.6-py2.7.egg-info/SOURCES.txt @@ -0,0 +1,121 @@ +README +setup.cfg +setup.py +South.egg-info/PKG-INFO +South.egg-info/SOURCES.txt +South.egg-info/dependency_links.txt +South.egg-info/top_level.txt +south/__init__.py +south/exceptions.py +south/logger.py +south/models.py +south/modelsinspector.py +south/orm.py +south/signals.py +south/v2.py +south/creator/__init__.py +south/creator/actions.py +south/creator/changes.py +south/creator/freezer.py +south/db/__init__.py +south/db/firebird.py +south/db/generic.py +south/db/mysql.py +south/db/oracle.py +south/db/postgresql_psycopg2.py +south/db/sqlite3.py +south/db/sql_server/__init__.py +south/db/sql_server/pyodbc.py +south/hacks/__init__.py +south/hacks/django_1_0.py +south/introspection_plugins/__init__.py +south/introspection_plugins/annoying_autoonetoone.py +south/introspection_plugins/django_audit_log.py +south/introspection_plugins/django_objectpermissions.py +south/introspection_plugins/django_tagging.py +south/introspection_plugins/django_taggit.py +south/introspection_plugins/django_timezones.py +south/introspection_plugins/geodjango.py +south/management/__init__.py +south/management/commands/__init__.py +south/management/commands/convert_to_south.py +south/management/commands/datamigration.py +south/management/commands/graphmigrations.py +south/management/commands/migrate.py +south/management/commands/migrationcheck.py +south/management/commands/schemamigration.py +south/management/commands/startmigration.py +south/management/commands/syncdb.py +south/management/commands/test.py +south/management/commands/testserver.py +south/migration/__init__.py +south/migration/base.py +south/migration/migrators.py +south/migration/utils.py +south/tests/__init__.py +south/tests/autodetection.py +south/tests/db.py +south/tests/db_mysql.py +south/tests/freezer.py +south/tests/inspector.py +south/tests/logger.py +south/tests/logic.py +south/tests/brokenapp/__init__.py +south/tests/brokenapp/models.py +south/tests/brokenapp/migrations/0001_depends_on_unmigrated.py +south/tests/brokenapp/migrations/0002_depends_on_unknown.py +south/tests/brokenapp/migrations/0003_depends_on_higher.py +south/tests/brokenapp/migrations/0004_higher.py +south/tests/brokenapp/migrations/__init__.py +south/tests/circular_a/__init__.py +south/tests/circular_a/models.py +south/tests/circular_a/migrations/0001_first.py +south/tests/circular_a/migrations/__init__.py +south/tests/circular_b/__init__.py +south/tests/circular_b/models.py +south/tests/circular_b/migrations/0001_first.py +south/tests/circular_b/migrations/__init__.py +south/tests/deps_a/__init__.py +south/tests/deps_a/models.py +south/tests/deps_a/migrations/0001_a.py +south/tests/deps_a/migrations/0002_a.py +south/tests/deps_a/migrations/0003_a.py +south/tests/deps_a/migrations/0004_a.py +south/tests/deps_a/migrations/0005_a.py +south/tests/deps_a/migrations/__init__.py +south/tests/deps_b/__init__.py +south/tests/deps_b/models.py +south/tests/deps_b/migrations/0001_b.py +south/tests/deps_b/migrations/0002_b.py +south/tests/deps_b/migrations/0003_b.py +south/tests/deps_b/migrations/0004_b.py +south/tests/deps_b/migrations/0005_b.py +south/tests/deps_b/migrations/__init__.py +south/tests/deps_c/__init__.py +south/tests/deps_c/models.py +south/tests/deps_c/migrations/0001_c.py +south/tests/deps_c/migrations/0002_c.py +south/tests/deps_c/migrations/0003_c.py +south/tests/deps_c/migrations/0004_c.py +south/tests/deps_c/migrations/0005_c.py +south/tests/deps_c/migrations/__init__.py +south/tests/emptyapp/__init__.py +south/tests/emptyapp/models.py +south/tests/emptyapp/migrations/__init__.py +south/tests/fakeapp/__init__.py +south/tests/fakeapp/models.py +south/tests/fakeapp/migrations/0001_spam.py +south/tests/fakeapp/migrations/0002_eggs.py +south/tests/fakeapp/migrations/0003_alter_spam.py +south/tests/fakeapp/migrations/__init__.py +south/tests/non_managed/__init__.py +south/tests/non_managed/models.py +south/tests/non_managed/migrations/__init__.py +south/tests/otherfakeapp/__init__.py +south/tests/otherfakeapp/models.py +south/tests/otherfakeapp/migrations/0001_first.py +south/tests/otherfakeapp/migrations/0002_second.py +south/tests/otherfakeapp/migrations/0003_third.py +south/tests/otherfakeapp/migrations/__init__.py +south/utils/__init__.py +south/utils/datetime_utils.py \ No newline at end of file diff --git a/lib/python/South-0.7.6-py2.7.egg-info/dependency_links.txt b/lib/python/South-0.7.6-py2.7.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lib/python/South-0.7.6-py2.7.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/lib/python/South-0.7.6-py2.7.egg-info/installed-files.txt b/lib/python/South-0.7.6-py2.7.egg-info/installed-files.txt new file mode 100644 index 0000000..adf66d4 --- /dev/null +++ b/lib/python/South-0.7.6-py2.7.egg-info/installed-files.txt @@ -0,0 +1,233 @@ +../south/__init__.py +../south/exceptions.py +../south/logger.py +../south/models.py +../south/modelsinspector.py +../south/orm.py +../south/signals.py +../south/v2.py +../south/creator/__init__.py +../south/creator/actions.py +../south/creator/changes.py +../south/creator/freezer.py +../south/db/__init__.py +../south/db/firebird.py +../south/db/generic.py +../south/db/mysql.py +../south/db/oracle.py +../south/db/postgresql_psycopg2.py +../south/db/sqlite3.py +../south/management/__init__.py +../south/introspection_plugins/__init__.py +../south/introspection_plugins/annoying_autoonetoone.py +../south/introspection_plugins/django_audit_log.py +../south/introspection_plugins/django_objectpermissions.py +../south/introspection_plugins/django_tagging.py +../south/introspection_plugins/django_taggit.py +../south/introspection_plugins/django_timezones.py +../south/introspection_plugins/geodjango.py +../south/hacks/__init__.py +../south/hacks/django_1_0.py +../south/migration/__init__.py +../south/migration/base.py +../south/migration/migrators.py +../south/migration/utils.py +../south/tests/__init__.py +../south/tests/autodetection.py +../south/tests/db.py +../south/tests/db_mysql.py +../south/tests/freezer.py +../south/tests/inspector.py +../south/tests/logger.py +../south/tests/logic.py +../south/db/sql_server/__init__.py +../south/db/sql_server/pyodbc.py +../south/management/commands/__init__.py +../south/management/commands/convert_to_south.py +../south/management/commands/datamigration.py +../south/management/commands/graphmigrations.py +../south/management/commands/migrate.py +../south/management/commands/migrationcheck.py +../south/management/commands/schemamigration.py +../south/management/commands/startmigration.py +../south/management/commands/syncdb.py +../south/management/commands/test.py +../south/management/commands/testserver.py +../south/tests/circular_a/__init__.py +../south/tests/circular_a/models.py +../south/tests/emptyapp/__init__.py +../south/tests/emptyapp/models.py +../south/tests/deps_a/__init__.py +../south/tests/deps_a/models.py +../south/tests/fakeapp/__init__.py +../south/tests/fakeapp/models.py +../south/tests/brokenapp/__init__.py +../south/tests/brokenapp/models.py +../south/tests/circular_b/__init__.py +../south/tests/circular_b/models.py +../south/tests/otherfakeapp/__init__.py +../south/tests/otherfakeapp/models.py +../south/tests/deps_c/__init__.py +../south/tests/deps_c/models.py +../south/tests/deps_b/__init__.py +../south/tests/deps_b/models.py +../south/tests/non_managed/__init__.py +../south/tests/non_managed/models.py +../south/tests/circular_a/migrations/0001_first.py +../south/tests/circular_a/migrations/__init__.py +../south/tests/emptyapp/migrations/__init__.py +../south/tests/deps_a/migrations/0001_a.py +../south/tests/deps_a/migrations/0002_a.py +../south/tests/deps_a/migrations/0003_a.py +../south/tests/deps_a/migrations/0004_a.py +../south/tests/deps_a/migrations/0005_a.py +../south/tests/deps_a/migrations/__init__.py +../south/tests/fakeapp/migrations/0001_spam.py +../south/tests/fakeapp/migrations/0002_eggs.py +../south/tests/fakeapp/migrations/0003_alter_spam.py +../south/tests/fakeapp/migrations/__init__.py +../south/tests/brokenapp/migrations/0001_depends_on_unmigrated.py +../south/tests/brokenapp/migrations/0002_depends_on_unknown.py +../south/tests/brokenapp/migrations/0003_depends_on_higher.py +../south/tests/brokenapp/migrations/0004_higher.py +../south/tests/brokenapp/migrations/__init__.py +../south/tests/circular_b/migrations/0001_first.py +../south/tests/circular_b/migrations/__init__.py +../south/tests/otherfakeapp/migrations/0001_first.py +../south/tests/otherfakeapp/migrations/0002_second.py +../south/tests/otherfakeapp/migrations/0003_third.py +../south/tests/otherfakeapp/migrations/__init__.py +../south/tests/deps_c/migrations/0001_c.py +../south/tests/deps_c/migrations/0002_c.py +../south/tests/deps_c/migrations/0003_c.py +../south/tests/deps_c/migrations/0004_c.py +../south/tests/deps_c/migrations/0005_c.py +../south/tests/deps_c/migrations/__init__.py +../south/tests/deps_b/migrations/0001_b.py +../south/tests/deps_b/migrations/0002_b.py +../south/tests/deps_b/migrations/0003_b.py +../south/tests/deps_b/migrations/0004_b.py +../south/tests/deps_b/migrations/0005_b.py +../south/tests/deps_b/migrations/__init__.py +../south/tests/non_managed/migrations/__init__.py +../south/utils/__init__.py +../south/utils/datetime_utils.py +../south/__init__.pyc +../south/exceptions.pyc +../south/logger.pyc +../south/models.pyc +../south/modelsinspector.pyc +../south/orm.pyc +../south/signals.pyc +../south/v2.pyc +../south/creator/__init__.pyc +../south/creator/actions.pyc +../south/creator/changes.pyc +../south/creator/freezer.pyc +../south/db/__init__.pyc +../south/db/firebird.pyc +../south/db/generic.pyc +../south/db/mysql.pyc +../south/db/oracle.pyc +../south/db/postgresql_psycopg2.pyc +../south/db/sqlite3.pyc +../south/management/__init__.pyc +../south/introspection_plugins/__init__.pyc +../south/introspection_plugins/annoying_autoonetoone.pyc +../south/introspection_plugins/django_audit_log.pyc +../south/introspection_plugins/django_objectpermissions.pyc +../south/introspection_plugins/django_tagging.pyc +../south/introspection_plugins/django_taggit.pyc +../south/introspection_plugins/django_timezones.pyc +../south/introspection_plugins/geodjango.pyc +../south/hacks/__init__.pyc +../south/hacks/django_1_0.pyc +../south/migration/__init__.pyc +../south/migration/base.pyc +../south/migration/migrators.pyc +../south/migration/utils.pyc +../south/tests/__init__.pyc +../south/tests/autodetection.pyc +../south/tests/db.pyc +../south/tests/db_mysql.pyc +../south/tests/freezer.pyc +../south/tests/inspector.pyc +../south/tests/logger.pyc +../south/tests/logic.pyc +../south/db/sql_server/__init__.pyc +../south/db/sql_server/pyodbc.pyc +../south/management/commands/__init__.pyc +../south/management/commands/convert_to_south.pyc +../south/management/commands/datamigration.pyc +../south/management/commands/graphmigrations.pyc +../south/management/commands/migrate.pyc +../south/management/commands/migrationcheck.pyc +../south/management/commands/schemamigration.pyc +../south/management/commands/startmigration.pyc +../south/management/commands/syncdb.pyc +../south/management/commands/test.pyc +../south/management/commands/testserver.pyc +../south/tests/circular_a/__init__.pyc +../south/tests/circular_a/models.pyc +../south/tests/emptyapp/__init__.pyc +../south/tests/emptyapp/models.pyc +../south/tests/deps_a/__init__.pyc +../south/tests/deps_a/models.pyc +../south/tests/fakeapp/__init__.pyc +../south/tests/fakeapp/models.pyc +../south/tests/brokenapp/__init__.pyc +../south/tests/brokenapp/models.pyc +../south/tests/circular_b/__init__.pyc +../south/tests/circular_b/models.pyc +../south/tests/otherfakeapp/__init__.pyc +../south/tests/otherfakeapp/models.pyc +../south/tests/deps_c/__init__.pyc +../south/tests/deps_c/models.pyc +../south/tests/deps_b/__init__.pyc +../south/tests/deps_b/models.pyc +../south/tests/non_managed/__init__.pyc +../south/tests/non_managed/models.pyc +../south/tests/circular_a/migrations/0001_first.pyc +../south/tests/circular_a/migrations/__init__.pyc +../south/tests/emptyapp/migrations/__init__.pyc +../south/tests/deps_a/migrations/0001_a.pyc +../south/tests/deps_a/migrations/0002_a.pyc +../south/tests/deps_a/migrations/0003_a.pyc +../south/tests/deps_a/migrations/0004_a.pyc +../south/tests/deps_a/migrations/0005_a.pyc +../south/tests/deps_a/migrations/__init__.pyc +../south/tests/fakeapp/migrations/0001_spam.pyc +../south/tests/fakeapp/migrations/0002_eggs.pyc +../south/tests/fakeapp/migrations/0003_alter_spam.pyc +../south/tests/fakeapp/migrations/__init__.pyc +../south/tests/brokenapp/migrations/0001_depends_on_unmigrated.pyc +../south/tests/brokenapp/migrations/0002_depends_on_unknown.pyc +../south/tests/brokenapp/migrations/0003_depends_on_higher.pyc +../south/tests/brokenapp/migrations/0004_higher.pyc +../south/tests/brokenapp/migrations/__init__.pyc +../south/tests/circular_b/migrations/0001_first.pyc +../south/tests/circular_b/migrations/__init__.pyc +../south/tests/otherfakeapp/migrations/0001_first.pyc +../south/tests/otherfakeapp/migrations/0002_second.pyc +../south/tests/otherfakeapp/migrations/0003_third.pyc +../south/tests/otherfakeapp/migrations/__init__.pyc +../south/tests/deps_c/migrations/0001_c.pyc +../south/tests/deps_c/migrations/0002_c.pyc +../south/tests/deps_c/migrations/0003_c.pyc +../south/tests/deps_c/migrations/0004_c.pyc +../south/tests/deps_c/migrations/0005_c.pyc +../south/tests/deps_c/migrations/__init__.pyc +../south/tests/deps_b/migrations/0001_b.pyc +../south/tests/deps_b/migrations/0002_b.pyc +../south/tests/deps_b/migrations/0003_b.pyc +../south/tests/deps_b/migrations/0004_b.pyc +../south/tests/deps_b/migrations/0005_b.pyc +../south/tests/deps_b/migrations/__init__.pyc +../south/tests/non_managed/migrations/__init__.pyc +../south/utils/__init__.pyc +../south/utils/datetime_utils.pyc +./ +dependency_links.txt +PKG-INFO +SOURCES.txt +top_level.txt diff --git a/lib/python/South-0.7.6-py2.7.egg-info/top_level.txt b/lib/python/South-0.7.6-py2.7.egg-info/top_level.txt new file mode 100644 index 0000000..40e0ef0 --- /dev/null +++ b/lib/python/South-0.7.6-py2.7.egg-info/top_level.txt @@ -0,0 +1 @@ +south diff --git a/lib/python/south/__init__.py b/lib/python/south/__init__.py new file mode 100644 index 0000000..163d396 --- /dev/null +++ b/lib/python/south/__init__.py @@ -0,0 +1,9 @@ +""" +South - Useable migrations for Django apps +""" + +__version__ = "0.7.6" +__authors__ = [ + "Andrew Godwin ", + "Andy McCurdy " +] diff --git a/lib/python/south/__init__.pyc b/lib/python/south/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9b48c4460de07fbe6adbeaea21ad8cdb256ae8c5 GIT binary patch literal 360 zcmYLEO-sZu5Y4u_%8LKML%>T9ZsWmg*5SR-wwWK!4uZvTMO;zwX! z=1pE6Z_=;x)keNNY4&pxx+^NYATW&W2p4R(U?yQEWlh3%)Eu!oJDyZm1gjc`_{P`# z8Lis4mUpfRDmw7UUm@`OcjX)4s_$`y)357%li%Pab+_IGd*BaX4$kvJ&9qWBC=JHu z5SsE=H}Xe)Hv}`zvaurV*l9gYat&R%%~CRqCj?R&&{D>;x&N>MDa#*M6cz$Xnb9-* z2P}vR+99CW2eJn*Y8NevIDA?I>(=eXRQ*T>Uf{H~kkWY&+C3qlMmEC}gffgy4eUmIPTRIw|#*W%T90bFr{NZ;qr zvHW}beQ9po`?GJ3Z=?2J!A2=G&71P3*|sO^-y23NdIRE>Z#sd0F%Qhe^SP%C@hV xL&2N^ZSWs%VyZ(yB^If`@IOD?p~?e9_U&Q!Ca`2k#>> ") + if not code: + print " ! Please enter some code, or 'exit' (with no quotes) to exit." + elif code == "exit": + sys.exit(1) + else: + try: + result = eval(code, {}, {"datetime": datetime_utils}) + except (SyntaxError, NameError), e: + print " ! Invalid input: %s" % e + else: + break + # Right, add the default in. + field_def[2]['default'] = value_clean(result) + + def irreversable_code(self, field): + return self.IRREVERSIBLE_TEMPLATE % { + "model_name": self.model._meta.object_name, + "table_name": self.model._meta.db_table, + "field_name": field.name, + "field_column": field.column, + } + + +class AddField(Action, _NullIssuesField): + """ + Adds a field to a model. Takes a Model class and the field name. + """ + + null_reason = "adding this field" + + FORWARDS_TEMPLATE = ''' + # Adding field '%(model_name)s.%(field_name)s' + db.add_column(%(table_name)r, %(field_name)r, + %(field_def)s, + keep_default=False)'''[1:] + "\n" + + BACKWARDS_TEMPLATE = ''' + # Deleting field '%(model_name)s.%(field_name)s' + db.delete_column(%(table_name)r, %(field_column)r)'''[1:] + "\n" + + def __init__(self, model, field, field_def): + self.model = model + self.field = field + self.field_def = field_def + + # See if they've made a NOT NULL column but also have no default (far too common) + is_null = self.field.null + default = (self.field.default is not None) and (self.field.default is not NOT_PROVIDED) + + if not is_null and not default: + self.deal_with_not_null_no_default(self.field, self.field_def) + + def console_line(self): + "Returns the string to print on the console, e.g. ' + Added field foo'" + return " + Added field %s on %s.%s" % ( + self.field.name, + self.model._meta.app_label, + self.model._meta.object_name, + ) + + def forwards_code(self): + + return self.FORWARDS_TEMPLATE % { + "model_name": self.model._meta.object_name, + "table_name": self.model._meta.db_table, + "field_name": self.field.name, + "field_column": self.field.column, + "field_def": self.triple_to_def(self.field_def), + } + + def backwards_code(self): + return self.BACKWARDS_TEMPLATE % { + "model_name": self.model._meta.object_name, + "table_name": self.model._meta.db_table, + "field_name": self.field.name, + "field_column": self.field.column, + } + + +class DeleteField(AddField): + """ + Removes a field from a model. Takes a Model class and the field name. + """ + + null_reason = "removing this field" + allow_third_null_option = True + + def console_line(self): + "Returns the string to print on the console, e.g. ' + Added field foo'" + return " - Deleted field %s on %s.%s" % ( + self.field.name, + self.model._meta.app_label, + self.model._meta.object_name, + ) + + def forwards_code(self): + return AddField.backwards_code(self) + + def backwards_code(self): + if not self.irreversible: + return AddField.forwards_code(self) + else: + return self.irreversable_code(self.field) + + +class ChangeField(Action, _NullIssuesField): + """ + Changes a field's type/options on a model. + """ + + null_reason = "making this field non-nullable" + + FORWARDS_TEMPLATE = BACKWARDS_TEMPLATE = ''' + # Changing field '%(model_name)s.%(field_name)s' + db.alter_column(%(table_name)r, %(field_column)r, %(field_def)s)''' + + RENAME_TEMPLATE = ''' + # Renaming column for '%(model_name)s.%(field_name)s' to match new field type. + db.rename_column(%(table_name)r, %(old_column)r, %(new_column)r)''' + + def __init__(self, model, old_field, new_field, old_def, new_def): + self.model = model + self.old_field = old_field + self.new_field = new_field + self.old_def = old_def + self.new_def = new_def + + # See if they've changed a not-null field to be null + new_default = (self.new_field.default is not None) and (self.new_field.default is not NOT_PROVIDED) + old_default = (self.old_field.default is not None) and (self.old_field.default is not NOT_PROVIDED) + if self.old_field.null and not self.new_field.null and not new_default: + self.deal_with_not_null_no_default(self.new_field, self.new_def) + if not self.old_field.null and self.new_field.null and not old_default: + self.null_reason = "making this field nullable" + self.allow_third_null_option = True + self.deal_with_not_null_no_default(self.old_field, self.old_def) + + def console_line(self): + "Returns the string to print on the console, e.g. ' + Added field foo'" + return " ~ Changed field %s on %s.%s" % ( + self.new_field.name, + self.model._meta.app_label, + self.model._meta.object_name, + ) + + def _code(self, old_field, new_field, new_def): + + output = "" + + if self.old_field.column != self.new_field.column: + output += self.RENAME_TEMPLATE % { + "model_name": self.model._meta.object_name, + "table_name": self.model._meta.db_table, + "field_name": new_field.name, + "old_column": old_field.column, + "new_column": new_field.column, + } + + output += self.FORWARDS_TEMPLATE % { + "model_name": self.model._meta.object_name, + "table_name": self.model._meta.db_table, + "field_name": new_field.name, + "field_column": new_field.column, + "field_def": self.triple_to_def(new_def), + } + + return output + + def forwards_code(self): + return self._code(self.old_field, self.new_field, self.new_def) + + def backwards_code(self): + if not self.irreversible: + return self._code(self.new_field, self.old_field, self.old_def) + else: + return self.irreversable_code(self.old_field) + + +class AddUnique(Action): + """ + Adds a unique constraint to a model. Takes a Model class and the field names. + """ + + FORWARDS_TEMPLATE = ''' + # Adding unique constraint on '%(model_name)s', fields %(field_names)s + db.create_unique(%(table_name)r, %(fields)r)'''[1:] + "\n" + + BACKWARDS_TEMPLATE = ''' + # Removing unique constraint on '%(model_name)s', fields %(field_names)s + db.delete_unique(%(table_name)r, %(fields)r)'''[1:] + "\n" + + prepend_backwards = True + + def __init__(self, model, fields): + self.model = model + self.fields = fields + + def console_line(self): + "Returns the string to print on the console, e.g. ' + Added field foo'" + return " + Added unique constraint for %s on %s.%s" % ( + [x.name for x in self.fields], + self.model._meta.app_label, + self.model._meta.object_name, + ) + + def forwards_code(self): + + return self.FORWARDS_TEMPLATE % { + "model_name": self.model._meta.object_name, + "table_name": self.model._meta.db_table, + "fields": [field.column for field in self.fields], + "field_names": [field.name for field in self.fields], + } + + def backwards_code(self): + return self.BACKWARDS_TEMPLATE % { + "model_name": self.model._meta.object_name, + "table_name": self.model._meta.db_table, + "fields": [field.column for field in self.fields], + "field_names": [field.name for field in self.fields], + } + + +class DeleteUnique(AddUnique): + """ + Removes a unique constraint from a model. Takes a Model class and the field names. + """ + + prepend_forwards = True + prepend_backwards = False + + def console_line(self): + "Returns the string to print on the console, e.g. ' + Added field foo'" + return " - Deleted unique constraint for %s on %s.%s" % ( + [x.name for x in self.fields], + self.model._meta.app_label, + self.model._meta.object_name, + ) + + def forwards_code(self): + return AddUnique.backwards_code(self) + + def backwards_code(self): + return AddUnique.forwards_code(self) + + +class AddIndex(AddUnique): + """ + Adds an index to a model field[s]. Takes a Model class and the field names. + """ + + FORWARDS_TEMPLATE = ''' + # Adding index on '%(model_name)s', fields %(field_names)s + db.create_index(%(table_name)r, %(fields)r)'''[1:] + "\n" + + BACKWARDS_TEMPLATE = ''' + # Removing index on '%(model_name)s', fields %(field_names)s + db.delete_index(%(table_name)r, %(fields)r)'''[1:] + "\n" + + def console_line(self): + "Returns the string to print on the console, e.g. ' + Added field foo'" + return " + Added index for %s on %s.%s" % ( + [x.name for x in self.fields], + self.model._meta.app_label, + self.model._meta.object_name, + ) + + +class DeleteIndex(AddIndex): + """ + Deletes an index off a model field[s]. Takes a Model class and the field names. + """ + + def console_line(self): + "Returns the string to print on the console, e.g. ' + Added field foo'" + return " + Deleted index for %s on %s.%s" % ( + [x.name for x in self.fields], + self.model._meta.app_label, + self.model._meta.object_name, + ) + + def forwards_code(self): + return AddIndex.backwards_code(self) + + def backwards_code(self): + return AddIndex.forwards_code(self) + + +class AddM2M(Action): + """ + Adds a unique constraint to a model. Takes a Model class and the field names. + """ + + FORWARDS_TEMPLATE = ''' + # Adding M2M table for field %(field_name)s on '%(model_name)s' + db.create_table(%(table_name)r, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + (%(left_field)r, models.ForeignKey(orm[%(left_model_key)r], null=False)), + (%(right_field)r, models.ForeignKey(orm[%(right_model_key)r], null=False)) + )) + db.create_unique(%(table_name)r, [%(left_column)r, %(right_column)r])'''[1:] + "\n" + + BACKWARDS_TEMPLATE = ''' + # Removing M2M table for field %(field_name)s on '%(model_name)s' + db.delete_table('%(table_name)s')'''[1:] + "\n" + + def __init__(self, model, field): + self.model = model + self.field = field + + def console_line(self): + "Returns the string to print on the console, e.g. ' + Added field foo'" + return " + Added M2M table for %s on %s.%s" % ( + self.field.name, + self.model._meta.app_label, + self.model._meta.object_name, + ) + + def forwards_code(self): + + return self.FORWARDS_TEMPLATE % { + "model_name": self.model._meta.object_name, + "field_name": self.field.name, + "table_name": self.field.m2m_db_table(), + "left_field": self.field.m2m_column_name()[:-3], # Remove the _id part + "left_column": self.field.m2m_column_name(), + "left_model_key": model_key(self.model), + "right_field": self.field.m2m_reverse_name()[:-3], # Remove the _id part + "right_column": self.field.m2m_reverse_name(), + "right_model_key": model_key(self.field.rel.to), + } + + def backwards_code(self): + + return self.BACKWARDS_TEMPLATE % { + "model_name": self.model._meta.object_name, + "field_name": self.field.name, + "table_name": self.field.m2m_db_table(), + } + + +class DeleteM2M(AddM2M): + """ + Adds a unique constraint to a model. Takes a Model class and the field names. + """ + + def console_line(self): + "Returns the string to print on the console, e.g. ' + Added field foo'" + return " - Deleted M2M table for %s on %s.%s" % ( + self.field.name, + self.model._meta.app_label, + self.model._meta.object_name, + ) + + def forwards_code(self): + return AddM2M.backwards_code(self) + + def backwards_code(self): + return AddM2M.forwards_code(self) + diff --git a/lib/python/south/creator/actions.pyc b/lib/python/south/creator/actions.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8552a226d38a8e33fdf591650a8e0b83d5274d9c GIT binary patch literal 23200 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 literal 0 HcmV?d00001 diff --git a/lib/python/south/creator/changes.py b/lib/python/south/creator/changes.py new file mode 100644 index 0000000..0570beb --- /dev/null +++ b/lib/python/south/creator/changes.py @@ -0,0 +1,489 @@ +""" +Contains things to detect changes - either using options passed in on the +commandline, or by using autodetection, etc. +""" + +from django.db import models +from django.contrib.contenttypes.generic import GenericRelation +from django.utils.datastructures import SortedDict + +from south.creator.freezer import remove_useless_attributes, freeze_apps, model_key +from south.utils import auto_through + +class BaseChanges(object): + """ + Base changes class. + """ + def suggest_name(self): + return '' + + def split_model_def(self, model, model_def): + """ + Given a model and its model def (a dict of field: triple), returns three + items: the real fields dict, the Meta dict, and the M2M fields dict. + """ + real_fields = SortedDict() + meta = SortedDict() + m2m_fields = SortedDict() + for name, triple in model_def.items(): + if name == "Meta": + meta = triple + elif isinstance(model._meta.get_field_by_name(name)[0], models.ManyToManyField): + m2m_fields[name] = triple + else: + real_fields[name] = triple + return real_fields, meta, m2m_fields + + def current_model_from_key(self, key): + app_label, model_name = key.split(".") + return models.get_model(app_label, model_name) + + def current_field_from_key(self, key, fieldname): + app_label, model_name = key.split(".") + # Special, for the magical field from order_with_respect_to + if fieldname == "_order": + field = models.IntegerField() + field.name = "_order" + field.attname = "_order" + field.column = "_order" + field.default = 0 + return field + # Otherwise, normal. + return models.get_model(app_label, model_name)._meta.get_field_by_name(fieldname)[0] + + +class AutoChanges(BaseChanges): + """ + Detects changes by 'diffing' two sets of frozen model definitions. + """ + + # Field types we don't generate add/remove field changes for. + IGNORED_FIELD_TYPES = [ + GenericRelation, + ] + + def __init__(self, migrations, old_defs, old_orm, new_defs): + self.migrations = migrations + self.old_defs = old_defs + self.old_orm = old_orm + self.new_defs = new_defs + + def suggest_name(self): + parts = ["auto"] + for change_name, params in self.get_changes(): + if change_name == "AddModel": + parts.append("add_%s" % params['model']._meta.object_name.lower()) + elif change_name == "DeleteModel": + parts.append("del_%s" % params['model']._meta.object_name.lower()) + elif change_name == "AddField": + parts.append("add_field_%s_%s" % ( + params['model']._meta.object_name.lower(), + params['field'].name, + )) + elif change_name == "DeleteField": + parts.append("del_field_%s_%s" % ( + params['model']._meta.object_name.lower(), + params['field'].name, + )) + elif change_name == "ChangeField": + parts.append("chg_field_%s_%s" % ( + params['model']._meta.object_name.lower(), + params['new_field'].name, + )) + elif change_name == "AddUnique": + parts.append("add_unique_%s_%s" % ( + params['model']._meta.object_name.lower(), + "_".join([x.name for x in params['fields']]), + )) + elif change_name == "DeleteUnique": + parts.append("del_unique_%s_%s" % ( + params['model']._meta.object_name.lower(), + "_".join([x.name for x in params['fields']]), + )) + return ("__".join(parts))[:70] + + def get_changes(self): + """ + Returns the difference between the old and new sets of models as a 5-tuple: + added_models, deleted_models, added_fields, deleted_fields, changed_fields + """ + + deleted_models = set() + + # See if anything's vanished + for key in self.old_defs: + if key not in self.new_defs: + # We shouldn't delete it if it was managed=False + old_fields, old_meta, old_m2ms = self.split_model_def(self.old_orm[key], self.old_defs[key]) + if old_meta.get("managed", "True") != "False": + # Alright, delete it. + yield ("DeleteModel", { + "model": self.old_orm[key], + "model_def": old_fields, + }) + # Also make sure we delete any M2Ms it had. + for fieldname in old_m2ms: + # Only delete its stuff if it wasn't a through=. + field = self.old_orm[key + ":" + fieldname] + if auto_through(field): + yield ("DeleteM2M", {"model": self.old_orm[key], "field": field}) + # And any unique constraints it had + unique_together = eval(old_meta.get("unique_together", "[]")) + if unique_together: + # If it's only a single tuple, make it into the longer one + if isinstance(unique_together[0], basestring): + unique_together = [unique_together] + # For each combination, make an action for it + for fields in unique_together: + yield ("DeleteUnique", { + "model": self.old_orm[key], + "fields": [self.old_orm[key]._meta.get_field_by_name(x)[0] for x in fields], + }) + # We always add it in here so we ignore it later + deleted_models.add(key) + + # Or appeared + for key in self.new_defs: + if key not in self.old_defs: + # We shouldn't add it if it's managed=False + new_fields, new_meta, new_m2ms = self.split_model_def(self.current_model_from_key(key), self.new_defs[key]) + if new_meta.get("managed", "True") != "False": + yield ("AddModel", { + "model": self.current_model_from_key(key), + "model_def": new_fields, + }) + # Also make sure we add any M2Ms it has. + for fieldname in new_m2ms: + # Only create its stuff if it wasn't a through=. + field = self.current_field_from_key(key, fieldname) + if auto_through(field): + yield ("AddM2M", {"model": self.current_model_from_key(key), "field": field}) + # And any unique constraints it has + unique_together = eval(new_meta.get("unique_together", "[]")) + if unique_together: + # If it's only a single tuple, make it into the longer one + if isinstance(unique_together[0], basestring): + unique_together = [unique_together] + # For each combination, make an action for it + for fields in unique_together: + yield ("AddUnique", { + "model": self.current_model_from_key(key), + "fields": [self.current_model_from_key(key)._meta.get_field_by_name(x)[0] for x in fields], + }) + + # Now, for every model that's stayed the same, check its fields. + for key in self.old_defs: + if key not in deleted_models: + + old_fields, old_meta, old_m2ms = self.split_model_def(self.old_orm[key], self.old_defs[key]) + new_fields, new_meta, new_m2ms = self.split_model_def(self.current_model_from_key(key), self.new_defs[key]) + + # Do nothing for models which are now not managed. + if new_meta.get("managed", "True") == "False": + continue + + # Find fields that have vanished. + for fieldname in old_fields: + if fieldname not in new_fields: + # Don't do it for any fields we're ignoring + field = self.old_orm[key + ":" + fieldname] + field_allowed = True + for field_type in self.IGNORED_FIELD_TYPES: + if isinstance(field, field_type): + field_allowed = False + if field_allowed: + # Looks alright. + yield ("DeleteField", { + "model": self.old_orm[key], + "field": field, + "field_def": old_fields[fieldname], + }) + + # And ones that have appeared + for fieldname in new_fields: + if fieldname not in old_fields: + # Don't do it for any fields we're ignoring + field = self.current_field_from_key(key, fieldname) + field_allowed = True + for field_type in self.IGNORED_FIELD_TYPES: + if isinstance(field, field_type): + field_allowed = False + if field_allowed: + # Looks alright. + yield ("AddField", { + "model": self.current_model_from_key(key), + "field": field, + "field_def": new_fields[fieldname], + }) + + # Find M2Ms that have vanished + for fieldname in old_m2ms: + if fieldname not in new_m2ms: + # Only delete its stuff if it wasn't a through=. + field = self.old_orm[key + ":" + fieldname] + if auto_through(field): + yield ("DeleteM2M", {"model": self.old_orm[key], "field": field}) + + # Find M2Ms that have appeared + for fieldname in new_m2ms: + if fieldname not in old_m2ms: + # Only create its stuff if it wasn't a through=. + field = self.current_field_from_key(key, fieldname) + if auto_through(field): + yield ("AddM2M", {"model": self.current_model_from_key(key), "field": field}) + + # For the ones that exist in both models, see if they were changed + for fieldname in set(old_fields).intersection(set(new_fields)): + # Non-index changes + if self.different_attributes( + remove_useless_attributes(old_fields[fieldname], True, True), + remove_useless_attributes(new_fields[fieldname], True, True)): + yield ("ChangeField", { + "model": self.current_model_from_key(key), + "old_field": self.old_orm[key + ":" + fieldname], + "new_field": self.current_field_from_key(key, fieldname), + "old_def": old_fields[fieldname], + "new_def": new_fields[fieldname], + }) + # Index changes + old_field = self.old_orm[key + ":" + fieldname] + new_field = self.current_field_from_key(key, fieldname) + if not old_field.db_index and new_field.db_index: + # They've added an index. + yield ("AddIndex", { + "model": self.current_model_from_key(key), + "fields": [new_field], + }) + if old_field.db_index and not new_field.db_index: + # They've removed an index. + yield ("DeleteIndex", { + "model": self.old_orm[key], + "fields": [old_field], + }) + # See if their uniques have changed + if old_field.unique != new_field.unique: + # Make sure we look at the one explicitly given to see what happened + if new_field.unique: + yield ("AddUnique", { + "model": self.current_model_from_key(key), + "fields": [new_field], + }) + else: + yield ("DeleteUnique", { + "model": self.old_orm[key], + "fields": [old_field], + }) + + # See if there's any M2Ms that have changed. + for fieldname in set(old_m2ms).intersection(set(new_m2ms)): + old_field = self.old_orm[key + ":" + fieldname] + new_field = self.current_field_from_key(key, fieldname) + # Have they _added_ a through= ? + if auto_through(old_field) and not auto_through(new_field): + yield ("DeleteM2M", {"model": self.old_orm[key], "field": old_field}) + # Have they _removed_ a through= ? + if not auto_through(old_field) and auto_through(new_field): + yield ("AddM2M", {"model": self.current_model_from_key(key), "field": new_field}) + + ## See if the unique_togethers have changed + # First, normalise them into lists of sets. + old_unique_together = eval(old_meta.get("unique_together", "[]")) + new_unique_together = eval(new_meta.get("unique_together", "[]")) + if old_unique_together and isinstance(old_unique_together[0], basestring): + old_unique_together = [old_unique_together] + if new_unique_together and isinstance(new_unique_together[0], basestring): + new_unique_together = [new_unique_together] + old_unique_together = map(set, old_unique_together) + new_unique_together = map(set, new_unique_together) + # See if any appeared or disappeared + for item in old_unique_together: + if item not in new_unique_together: + yield ("DeleteUnique", { + "model": self.old_orm[key], + "fields": [self.old_orm[key + ":" + x] for x in item], + }) + for item in new_unique_together: + if item not in old_unique_together: + yield ("AddUnique", { + "model": self.current_model_from_key(key), + "fields": [self.current_field_from_key(key, x) for x in item], + }) + + @classmethod + def is_triple(cls, triple): + "Returns whether the argument is a triple." + return isinstance(triple, (list, tuple)) and len(triple) == 3 and \ + isinstance(triple[0], (str, unicode)) and \ + isinstance(triple[1], (list, tuple)) and \ + isinstance(triple[2], dict) + + @classmethod + def different_attributes(cls, old, new): + """ + Backwards-compat comparison that ignores orm. on the RHS and not the left + and which knows django.db.models.fields.CharField = models.CharField. + Has a whole load of tests in tests/autodetection.py. + """ + + # If they're not triples, just do normal comparison + if not cls.is_triple(old) or not cls.is_triple(new): + return old != new + + # Expand them out into parts + old_field, old_pos, old_kwd = old + new_field, new_pos, new_kwd = new + + # Copy the positional and keyword arguments so we can compare them and pop off things + old_pos, new_pos = old_pos[:], new_pos[:] + old_kwd = dict(old_kwd.items()) + new_kwd = dict(new_kwd.items()) + + # Remove comparison of the existence of 'unique', that's done elsewhere. + # TODO: Make this work for custom fields where unique= means something else? + if "unique" in old_kwd: + del old_kwd['unique'] + if "unique" in new_kwd: + del new_kwd['unique'] + + # If the first bit is different, check it's not by dj.db.models... + if old_field != new_field: + if old_field.startswith("models.") and (new_field.startswith("django.db.models") \ + or new_field.startswith("django.contrib.gis")): + if old_field.split(".")[-1] != new_field.split(".")[-1]: + return True + else: + # Remove those fields from the final comparison + old_field = new_field = "" + + # If there's a positional argument in the first, and a 'to' in the second, + # see if they're actually comparable. + if (old_pos and "to" in new_kwd) and ("orm" in new_kwd['to'] and "orm" not in old_pos[0]): + # Do special comparison to fix #153 + try: + if old_pos[0] != new_kwd['to'].split("'")[1].split(".")[1]: + return True + except IndexError: + pass # Fall back to next comparison + # Remove those attrs from the final comparison + old_pos = old_pos[1:] + del new_kwd['to'] + + return old_field != new_field or old_pos != new_pos or old_kwd != new_kwd + + +class ManualChanges(BaseChanges): + """ + Detects changes by reading the command line. + """ + + def __init__(self, migrations, added_models, added_fields, added_indexes): + self.migrations = migrations + self.added_models = added_models + self.added_fields = added_fields + self.added_indexes = added_indexes + + def suggest_name(self): + bits = [] + for model_name in self.added_models: + bits.append('add_model_%s' % model_name) + for field_name in self.added_fields: + bits.append('add_field_%s' % field_name) + for index_name in self.added_indexes: + bits.append('add_index_%s' % index_name) + return '_'.join(bits).replace('.', '_') + + def get_changes(self): + # Get the model defs so we can use them for the yield later + model_defs = freeze_apps([self.migrations.app_label()]) + # Make the model changes + for model_name in self.added_models: + model = models.get_model(self.migrations.app_label(), model_name) + real_fields, meta, m2m_fields = self.split_model_def(model, model_defs[model_key(model)]) + yield ("AddModel", { + "model": model, + "model_def": real_fields, + }) + # And the field changes + for field_desc in self.added_fields: + try: + model_name, field_name = field_desc.split(".") + except (TypeError, ValueError): + raise ValueError("%r is not a valid field description." % field_desc) + model = models.get_model(self.migrations.app_label(), model_name) + real_fields, meta, m2m_fields = self.split_model_def(model, model_defs[model_key(model)]) + yield ("AddField", { + "model": model, + "field": model._meta.get_field_by_name(field_name)[0], + "field_def": real_fields[field_name], + }) + # And the indexes + for field_desc in self.added_indexes: + try: + model_name, field_name = field_desc.split(".") + except (TypeError, ValueError): + print "%r is not a valid field description." % field_desc + model = models.get_model(self.migrations.app_label(), model_name) + yield ("AddIndex", { + "model": model, + "fields": [model._meta.get_field_by_name(field_name)[0]], + }) + + +class InitialChanges(BaseChanges): + """ + Creates all models; handles --initial. + """ + def suggest_name(self): + return 'initial' + + def __init__(self, migrations): + self.migrations = migrations + + def get_changes(self): + # Get the frozen models for this app + model_defs = freeze_apps([self.migrations.app_label()]) + + for model in models.get_models(models.get_app(self.migrations.app_label())): + + # Don't do anything for unmanaged, abstract or proxy models + if model._meta.abstract or getattr(model._meta, "proxy", False) or not getattr(model._meta, "managed", True): + continue + + real_fields, meta, m2m_fields = self.split_model_def(model, model_defs[model_key(model)]) + + # Firstly, add the main table and fields + yield ("AddModel", { + "model": model, + "model_def": real_fields, + }) + + # Then, add any uniqueness that's around + if meta: + unique_together = eval(meta.get("unique_together", "[]")) + if unique_together: + # If it's only a single tuple, make it into the longer one + if isinstance(unique_together[0], basestring): + unique_together = [unique_together] + # For each combination, make an action for it + for fields in unique_together: + yield ("AddUnique", { + "model": model, + "fields": [model._meta.get_field_by_name(x)[0] for x in fields], + }) + + # Finally, see if there's some M2M action + for name, triple in m2m_fields.items(): + field = model._meta.get_field_by_name(name)[0] + # But only if it's not through=foo (#120) + if field.rel.through: + try: + # Django 1.1 and below + through_model = field.rel.through_model + except AttributeError: + # Django 1.2 + through_model = field.rel.through + if (not field.rel.through) or getattr(through_model._meta, "auto_created", False): + yield ("AddM2M", { + "model": model, + "field": field, + }) diff --git a/lib/python/south/creator/changes.pyc b/lib/python/south/creator/changes.pyc new file mode 100644 index 0000000000000000000000000000000000000000..52d411a90d576326cf6400591424ccdcb66c2f70 GIT binary patch literal 13247 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 literal 0 HcmV?d00001 diff --git a/lib/python/south/creator/freezer.py b/lib/python/south/creator/freezer.py new file mode 100644 index 0000000..3c78e45 --- /dev/null +++ b/lib/python/south/creator/freezer.py @@ -0,0 +1,190 @@ +""" +Handles freezing of models into FakeORMs. +""" + +import sys + +from django.db import models +from django.db.models.base import ModelBase, Model +from django.contrib.contenttypes.generic import GenericRelation + +from south.orm import FakeORM +from south.utils import get_attribute, auto_through +from south import modelsinspector + +def freeze_apps(apps): + """ + Takes a list of app labels, and returns a string of their frozen form. + """ + if isinstance(apps, basestring): + apps = [apps] + frozen_models = set() + # For each app, add in all its models + for app in apps: + for model in models.get_models(models.get_app(app)): + # Only add if it's not abstract or proxy + if not model._meta.abstract and not getattr(model._meta, "proxy", False): + frozen_models.add(model) + # Now, add all the dependencies + for model in list(frozen_models): + frozen_models.update(model_dependencies(model)) + # Serialise! + model_defs = {} + model_classes = {} + for model in frozen_models: + model_defs[model_key(model)] = prep_for_freeze(model) + model_classes[model_key(model)] = model + # Check for any custom fields that failed to freeze. + missing_fields = False + for key, fields in model_defs.items(): + for field_name, value in fields.items(): + if value is None: + missing_fields = True + model_class = model_classes[key] + field_class = model_class._meta.get_field_by_name(field_name)[0] + print " ! Cannot freeze field '%s.%s'" % (key, field_name) + print " ! (this field has class %s.%s)" % (field_class.__class__.__module__, field_class.__class__.__name__) + if missing_fields: + print "" + print " ! South cannot introspect some fields; this is probably because they are custom" + print " ! fields. If they worked in 0.6 or below, this is because we have removed the" + print " ! models parser (it often broke things)." + print " ! To fix this, read http://south.aeracode.org/wiki/MyFieldsDontWork" + sys.exit(1) + + return model_defs + +def freeze_apps_to_string(apps): + return pprint_frozen_models(freeze_apps(apps)) + +### + +def model_key(model): + "For a given model, return 'appname.modelname'." + return "%s.%s" % (model._meta.app_label, model._meta.object_name.lower()) + +def prep_for_freeze(model): + """ + Takes a model and returns the ready-to-serialise dict (all you need + to do is just pretty-print it). + """ + fields = modelsinspector.get_model_fields(model, m2m=True) + # Remove useless attributes (like 'choices') + for name, field in fields.items(): + fields[name] = remove_useless_attributes(field) + # See if there's a Meta + fields['Meta'] = remove_useless_meta(modelsinspector.get_model_meta(model)) + # Add in our own special items to track the object name and managed + fields['Meta']['object_name'] = model._meta.object_name # Special: not eval'able. + if not getattr(model._meta, "managed", True): + fields['Meta']['managed'] = repr(model._meta.managed) + return fields + +### Dependency resolvers + +def model_dependencies(model, checked_models=None): + """ + Returns a set of models this one depends on to be defined; things like + OneToOneFields as ID, ForeignKeys everywhere, etc. + """ + depends = set() + checked_models = checked_models or set() + # Get deps for each field + for field in model._meta.fields + model._meta.many_to_many: + depends.update(field_dependencies(field, checked_models)) + # Add in any non-abstract bases + for base in model.__bases__: + if issubclass(base, models.Model) and hasattr(base, '_meta') and not base._meta.abstract: + depends.add(base) + # Now recurse + new_to_check = depends - checked_models + while new_to_check: + checked_model = new_to_check.pop() + if checked_model == model or checked_model in checked_models: + continue + checked_models.add(checked_model) + deps = model_dependencies(checked_model, checked_models) + # Loop through dependencies... + for dep in deps: + # If the new dep is not already checked, add to the queue + if (dep not in depends) and (dep not in new_to_check) and (dep not in checked_models): + new_to_check.add(dep) + depends.add(dep) + return depends + +def field_dependencies(field, checked_models=None): + checked_models = checked_models or set() + depends = set() + arg_defs, kwarg_defs = modelsinspector.matching_details(field) + for attrname, options in arg_defs + kwarg_defs.values(): + if options.get("ignore_if_auto_through", False) and auto_through(field): + continue + if options.get("is_value", False): + value = attrname + elif attrname == 'rel.through' and hasattr(getattr(field, 'rel', None), 'through_model'): + # Hack for django 1.1 and below, where the through model is stored + # in rel.through_model while rel.through stores only the model name. + value = field.rel.through_model + else: + try: + value = get_attribute(field, attrname) + except AttributeError: + if options.get("ignore_missing", False): + continue + raise + if isinstance(value, Model): + value = value.__class__ + if not isinstance(value, ModelBase): + continue + if getattr(value._meta, "proxy", False): + value = value._meta.proxy_for_model + if value in checked_models: + continue + checked_models.add(value) + depends.add(value) + depends.update(model_dependencies(value, checked_models)) + + return depends + +### Prettyprinters + +def pprint_frozen_models(models): + return "{\n %s\n }" % ",\n ".join([ + "%r: %s" % (name, pprint_fields(fields)) + for name, fields in sorted(models.items()) + ]) + +def pprint_fields(fields): + return "{\n %s\n }" % ",\n ".join([ + "%r: %r" % (name, defn) + for name, defn in sorted(fields.items()) + ]) + +### Output sanitisers + +USELESS_KEYWORDS = ["choices", "help_text", "verbose_name"] +USELESS_DB_KEYWORDS = ["related_name", "default", "blank"] # Important for ORM, not for DB. +INDEX_KEYWORDS = ["db_index"] + +def remove_useless_attributes(field, db=False, indexes=False): + "Removes useless (for database) attributes from the field's defn." + # Work out what to remove, and remove it. + keywords = USELESS_KEYWORDS[:] + if db: + keywords += USELESS_DB_KEYWORDS[:] + if indexes: + keywords += INDEX_KEYWORDS[:] + if field: + for name in keywords: + if name in field[2]: + del field[2][name] + return field + +USELESS_META = ["verbose_name", "verbose_name_plural"] +def remove_useless_meta(meta): + "Removes useless (for database) attributes from the table's meta." + if meta: + for name in USELESS_META: + if name in meta: + del meta[name] + return meta diff --git a/lib/python/south/creator/freezer.pyc b/lib/python/south/creator/freezer.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a5a7fbde1e3243b8b8f2b2c8f536a40fbc1c622f GIT binary patch literal 6986 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 +dbs = {} +try: + for alias, module_name in db_engines.items(): + module = __import__(module_name, {}, {}, ['']) + dbs[alias] = module.DatabaseOperations(alias) +except ImportError: + # This error should only be triggered on 1.1 and below. + sys.stderr.write( + ( + "There is no South database module '%s' for your database. " + \ + "Please either choose a supported database, check for " + \ + "SOUTH_DATABASE_ADAPTER[S] settings, " + \ + "or remove South from INSTALLED_APPS.\n" + ) % (module_name,) + ) + sys.exit(1) + +# Finally, to make old migrations work, keep 'db' around as the default database +db = dbs[DEFAULT_DB_ALIAS] diff --git a/lib/python/south/db/__init__.pyc b/lib/python/south/db/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..32b7c01821f74a09385f396c253f6dff0a6dce47 GIT binary patch literal 2095 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 sys.stderr, 'FATAL ERROR - The following SQL query failed: %s' % sql + print >> sys.stderr, 'The error was: %s' % e + raise + + try: + return cursor.fetchall() + except: + return [] + + def execute_many(self, sql, regex=r"(?mx) ([^';]* (?:'[^']*'[^';]*)*)", comment_regex=r"(?mx) (?:^\s*$)|(?:--.*$)"): + """ + Takes a SQL file and executes it as many separate statements. + (Some backends, such as Postgres, don't work otherwise.) + """ + # Be warned: This function is full of dark magic. Make sure you really + # know regexes before trying to edit it. + # First, strip comments + sql = "\n".join([x.strip().replace("%", "%%") for x in re.split(comment_regex, sql) if x.strip()]) + # Now execute each statement + for st in re.split(regex, sql)[1:][::2]: + self.execute(st) + + def add_deferred_sql(self, sql): + """ + Add a SQL statement to the deferred list, that won't be executed until + this instance's execute_deferred_sql method is run. + """ + self.deferred_sql.append(sql) + + def execute_deferred_sql(self): + """ + Executes all deferred SQL, resetting the deferred_sql list + """ + for sql in self.deferred_sql: + self.execute(sql) + + self.deferred_sql = [] + + def clear_deferred_sql(self): + """ + Resets the deferred_sql list to empty. + """ + self.deferred_sql = [] + + def clear_run_data(self, pending_creates = None): + """ + Resets variables to how they should be before a run. Used for dry runs. + If you want, pass in an old panding_creates to reset to. + """ + self.clear_deferred_sql() + self.pending_create_signals = pending_creates or [] + + def get_pending_creates(self): + return self.pending_create_signals + + @invalidate_table_constraints + def create_table(self, table_name, fields): + """ + Creates the table 'table_name'. 'fields' is a tuple of fields, + each repsented by a 2-part tuple of field name and a + django.db.models.fields.Field object + """ + + if len(table_name) > 63: + print " ! WARNING: You have a table name longer than 63 characters; this will not fully work on PostgreSQL or MySQL." + + # avoid default values in CREATE TABLE statements (#925) + for field_name, field in fields: + field._suppress_default = True + + columns = [ + self.column_sql(table_name, field_name, field) + for field_name, field in fields + ] + + self.execute('CREATE TABLE %s (%s);' % ( + self.quote_name(table_name), + ', '.join([col for col in columns if col]), + )) + + add_table = alias('create_table') # Alias for consistency's sake + + @invalidate_table_constraints + def rename_table(self, old_table_name, table_name): + """ + Renames the table 'old_table_name' to 'table_name'. + """ + if old_table_name == table_name: + # Short-circuit out. + return + params = (self.quote_name(old_table_name), self.quote_name(table_name)) + self.execute(self.rename_table_sql % params) + # Invalidate the not-yet-indexed table + self._set_cache(table_name, value=INVALID) + + @invalidate_table_constraints + def delete_table(self, table_name, cascade=True): + """ + Deletes the table 'table_name'. + """ + params = (self.quote_name(table_name), ) + if cascade: + self.execute('DROP TABLE %s CASCADE;' % params) + else: + self.execute('DROP TABLE %s;' % params) + + drop_table = alias('delete_table') + + @invalidate_table_constraints + def clear_table(self, table_name): + """ + Deletes all rows from 'table_name'. + """ + params = (self.quote_name(table_name), ) + self.execute('DELETE FROM %s;' % params) + + @invalidate_table_constraints + def add_column(self, table_name, name, field, keep_default=True): + """ + Adds the column 'name' to the table 'table_name'. + Uses the 'field' paramater, a django.db.models.fields.Field instance, + to generate the necessary sql + + @param table_name: The name of the table to add the column to + @param name: The name of the column to add + @param field: The field to use + """ + sql = self.column_sql(table_name, name, field) + if sql: + params = ( + self.quote_name(table_name), + sql, + ) + sql = self.add_column_string % params + self.execute(sql) + + # Now, drop the default if we need to + if not keep_default and field.default is not None: + field.default = NOT_PROVIDED + self.alter_column(table_name, name, field, explicit_name=False, ignore_constraints=True) + + def _db_type_for_alter_column(self, field): + """ + Returns a field's type suitable for ALTER COLUMN. + By default it just returns field.db_type(). + To be overriden by backend specific subclasses + @param field: The field to generate type for + """ + try: + return field.db_type(connection=self._get_connection()) + except TypeError: + return field.db_type() + + def _alter_add_column_mods(self, field, name, params, sqls): + """ + Subcommand of alter_column that modifies column definitions beyond + the type string -- e.g. adding constraints where they cannot be specified + as part of the type (overrideable) + """ + pass + + def _alter_set_defaults(self, field, name, params, sqls): + "Subcommand of alter_column that sets default values (overrideable)" + # Next, set any default + if not field.null and field.has_default(): + default = field.get_default() + sqls.append(('ALTER COLUMN %s SET DEFAULT %%s ' % (self.quote_name(name),), [default])) + else: + sqls.append(('ALTER COLUMN %s DROP DEFAULT' % (self.quote_name(name),), [])) + + @invalidate_table_constraints + def alter_column(self, table_name, name, field, explicit_name=True, ignore_constraints=False): + """ + Alters the given column name so it will match the given field. + Note that conversion between the two by the database must be possible. + Will not automatically add _id by default; to have this behavour, pass + explicit_name=False. + + @param table_name: The name of the table to add the column to + @param name: The name of the column to alter + @param field: The new field definition to use + """ + + if self.dry_run: + if self.debug: + print ' - no dry run output for alter_column() due to dynamic DDL, sorry' + return + + # hook for the field to do any resolution prior to it's attributes being queried + if hasattr(field, 'south_init'): + field.south_init() + + # Add _id or whatever if we need to + field.set_attributes_from_name(name) + if not explicit_name: + name = field.column + else: + field.column = name + + if not ignore_constraints: + # Drop all check constraints. Note that constraints will be added back + # with self.alter_string_set_type and self.alter_string_drop_null. + if self.has_check_constraints: + check_constraints = self._constraints_affecting_columns(table_name, [name], "CHECK") + for constraint in check_constraints: + self.execute(self.delete_check_sql % { + 'table': self.quote_name(table_name), + 'constraint': self.quote_name(constraint), + }) + + # Drop all foreign key constraints + try: + self.delete_foreign_key(table_name, name) + except ValueError: + # There weren't any + pass + + # First, change the type + params = { + "column": self.quote_name(name), + "type": self._db_type_for_alter_column(field), + "table_name": table_name + } + + # SQLs is a list of (SQL, values) pairs. + sqls = [] + + # Only alter the column if it has a type (Geometry ones sometimes don't) + if params["type"] is not None: + sqls.append((self.alter_string_set_type % params, [])) + + # Add any field- and backend- specific modifications + self._alter_add_column_mods(field, name, params, sqls) + # Next, nullity + if field.null: + sqls.append((self.alter_string_set_null % params, [])) + else: + sqls.append((self.alter_string_drop_null % params, [])) + + # Next, set any default + self._alter_set_defaults(field, name, params, sqls) + + # Finally, actually change the column + if self.allows_combined_alters: + sqls, values = zip(*sqls) + self.execute( + "ALTER TABLE %s %s;" % (self.quote_name(table_name), ", ".join(sqls)), + flatten(values), + ) + else: + # Databases like e.g. MySQL don't like more than one alter at once. + for sql, values in sqls: + self.execute("ALTER TABLE %s %s;" % (self.quote_name(table_name), sql), values) + + if not ignore_constraints: + # Add back FK constraints if needed + if field.rel and self.supports_foreign_keys: + self.execute( + self.foreign_key_sql( + table_name, + field.column, + field.rel.to._meta.db_table, + field.rel.to._meta.get_field(field.rel.field_name).column + ) + ) + + def _fill_constraint_cache(self, db_name, table_name): + + schema = self._get_schema_name() + ifsc_tables = ["constraint_column_usage", "key_column_usage"] + + self._constraint_cache.setdefault(db_name, {}) + self._constraint_cache[db_name][table_name] = {} + + for ifsc_table in ifsc_tables: + rows = self.execute(""" + SELECT kc.constraint_name, kc.column_name, c.constraint_type + FROM information_schema.%s AS kc + JOIN information_schema.table_constraints AS c ON + kc.table_schema = c.table_schema AND + kc.table_name = c.table_name AND + kc.constraint_name = c.constraint_name + WHERE + kc.table_schema = %%s AND + kc.table_name = %%s + """ % ifsc_table, [schema, table_name]) + for constraint, column, kind in rows: + self._constraint_cache[db_name][table_name].setdefault(column, set()) + self._constraint_cache[db_name][table_name][column].add((kind, constraint)) + return + + def _constraints_affecting_columns(self, table_name, columns, type="UNIQUE"): + """ + Gets the names of the constraints affecting the given columns. + If columns is None, returns all constraints of the type on the table. + """ + if self.dry_run: + raise DryRunError("Cannot get constraints for columns.") + + if columns is not None: + columns = set(map(lambda s: s.lower(), columns)) + + db_name = self._get_setting('NAME') + + cnames = {} + for col, constraints in self.lookup_constraint(db_name, table_name): + for kind, cname in constraints: + if kind == type: + cnames.setdefault(cname, set()) + cnames[cname].add(col.lower()) + + for cname, cols in cnames.items(): + if cols == columns or columns is None: + yield cname + + @invalidate_table_constraints + def create_unique(self, table_name, columns): + """ + Creates a UNIQUE constraint on the columns on the given table. + """ + + if not isinstance(columns, (list, tuple)): + columns = [columns] + + name = self.create_index_name(table_name, columns, suffix="_uniq") + + cols = ", ".join(map(self.quote_name, columns)) + self.execute("ALTER TABLE %s ADD CONSTRAINT %s UNIQUE (%s)" % ( + self.quote_name(table_name), + self.quote_name(name), + cols, + )) + return name + + @invalidate_table_constraints + def delete_unique(self, table_name, columns): + """ + Deletes a UNIQUE constraint on precisely the columns on the given table. + """ + + if not isinstance(columns, (list, tuple)): + columns = [columns] + + # Dry runs mean we can't do anything. + if self.dry_run: + if self.debug: + print ' - no dry run output for delete_unique_column() due to dynamic DDL, sorry' + return + + constraints = list(self._constraints_affecting_columns(table_name, columns)) + if not constraints: + raise ValueError("Cannot find a UNIQUE constraint on table %s, columns %r" % (table_name, columns)) + for constraint in constraints: + self.execute(self.delete_unique_sql % ( + self.quote_name(table_name), + self.quote_name(constraint), + )) + + def column_sql(self, table_name, field_name, field, tablespace='', with_name=True, field_prepared=False): + """ + Creates the SQL snippet for a column. Used by add_column and add_table. + """ + + # If the field hasn't already been told its attribute name, do so. + if not field_prepared: + field.set_attributes_from_name(field_name) + + # hook for the field to do any resolution prior to it's attributes being queried + if hasattr(field, 'south_init'): + field.south_init() + + # Possible hook to fiddle with the fields (e.g. defaults & TEXT on MySQL) + field = self._field_sanity(field) + + try: + sql = field.db_type(connection=self._get_connection()) + except TypeError: + sql = field.db_type() + + if sql: + + # Some callers, like the sqlite stuff, just want the extended type. + if with_name: + field_output = [self.quote_name(field.column), sql] + else: + field_output = [sql] + + field_output.append('%sNULL' % (not field.null and 'NOT ' or '')) + if field.primary_key: + field_output.append('PRIMARY KEY') + elif field.unique: + # Just use UNIQUE (no indexes any more, we have delete_unique) + field_output.append('UNIQUE') + + tablespace = field.db_tablespace or tablespace + if tablespace and getattr(self._get_connection().features, "supports_tablespaces", False) and field.unique: + # We must specify the index tablespace inline, because we + # won't be generating a CREATE INDEX statement for this field. + field_output.append(self._get_connection().ops.tablespace_sql(tablespace, inline=True)) + + sql = ' '.join(field_output) + sqlparams = () + # if the field is "NOT NULL" and a default value is provided, create the column with it + # this allows the addition of a NOT NULL field to a table with existing rows + if not getattr(field, '_suppress_default', False): + if field.has_default(): + default = field.get_default() + # If the default is actually None, don't add a default term + if default is not None: + # If the default is a callable, then call it! + if callable(default): + default = default() + + default = field.get_db_prep_save(default, connection=self._get_connection()) + default = self._default_value_workaround(default) + # Now do some very cheap quoting. TODO: Redesign return values to avoid this. + if isinstance(default, basestring): + default = "'%s'" % default.replace("'", "''") + # Escape any % signs in the output (bug #317) + if isinstance(default, basestring): + default = default.replace("%", "%%") + # Add it in + sql += " DEFAULT %s" + sqlparams = (default) + elif (not field.null and field.blank) or (field.get_default() == ''): + if field.empty_strings_allowed and self._get_connection().features.interprets_empty_strings_as_nulls: + sql += " DEFAULT ''" + # Error here would be nice, but doesn't seem to play fair. + #else: + # raise ValueError("Attempting to add a non null column that isn't character based without an explicit default value.") + + if field.rel and self.supports_foreign_keys: + self.add_deferred_sql( + self.foreign_key_sql( + table_name, + field.column, + field.rel.to._meta.db_table, + field.rel.to._meta.get_field(field.rel.field_name).column + ) + ) + + # Things like the contrib.gis module fields have this in 1.1 and below + if hasattr(field, 'post_create_sql'): + for stmt in field.post_create_sql(no_style(), table_name): + self.add_deferred_sql(stmt) + + # In 1.2 and above, you have to ask the DatabaseCreation stuff for it. + # This also creates normal indexes in 1.1. + if hasattr(self._get_connection().creation, "sql_indexes_for_field"): + # Make a fake model to pass in, with only db_table + model = self.mock_model("FakeModelForGISCreation", table_name) + for stmt in self._get_connection().creation.sql_indexes_for_field(model, field, no_style()): + self.add_deferred_sql(stmt) + + if sql: + return sql % sqlparams + else: + return None + + def _field_sanity(self, field): + """ + Placeholder for DBMS-specific field alterations (some combos aren't valid, + e.g. DEFAULT and TEXT on MySQL) + """ + return field + + def _default_value_workaround(self, value): + """ + DBMS-specific value alterations (this really works around + missing functionality in Django backends) + """ + if isinstance(value, bool) and not self.has_booleans: + return int(value) + else: + return value + + def foreign_key_sql(self, from_table_name, from_column_name, to_table_name, to_column_name): + """ + Generates a full SQL statement to add a foreign key constraint + """ + constraint_name = '%s_refs_%s_%x' % (from_column_name, to_column_name, abs(hash((from_table_name, to_table_name)))) + return 'ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' % ( + self.quote_name(from_table_name), + self.quote_name(truncate_name(constraint_name, self._get_connection().ops.max_name_length())), + self.quote_name(from_column_name), + self.quote_name(to_table_name), + self.quote_name(to_column_name), + self._get_connection().ops.deferrable_sql() # Django knows this + ) + + @invalidate_table_constraints + def delete_foreign_key(self, table_name, column): + """ + Drop a foreign key constraint + """ + if self.dry_run: + if self.debug: + print ' - no dry run output for delete_foreign_key() due to dynamic DDL, sorry' + return # We can't look at the DB to get the constraints + constraints = self._find_foreign_constraints(table_name, column) + if not constraints: + raise ValueError("Cannot find a FOREIGN KEY constraint on table %s, column %s" % (table_name, column)) + for constraint_name in constraints: + self.execute(self.delete_foreign_key_sql % { + "table": self.quote_name(table_name), + "constraint": self.quote_name(constraint_name), + }) + + drop_foreign_key = alias('delete_foreign_key') + + def _find_foreign_constraints(self, table_name, column_name=None): + constraints = self._constraints_affecting_columns( + table_name, [column_name], "FOREIGN KEY") + + primary_key_columns = self._find_primary_key_columns(table_name) + + if len(primary_key_columns) > 1: + # Composite primary keys cannot be referenced by a foreign key + return list(constraints) + else: + primary_key_columns.add(column_name) + recursive_constraints = set(self._constraints_affecting_columns( + table_name, primary_key_columns, "FOREIGN KEY")) + return list(recursive_constraints.union(constraints)) + + def _digest(self, *args): + """ + Use django.db.backends.creation.BaseDatabaseCreation._digest + to create index name in Django style. An evil hack :( + """ + if not hasattr(self, '_django_db_creation'): + self._django_db_creation = BaseDatabaseCreation(self._get_connection()) + return self._django_db_creation._digest(*args) + + def create_index_name(self, table_name, column_names, suffix=""): + """ + Generate a unique name for the index + """ + + # If there is just one column in the index, use a default algorithm from Django + if len(column_names) == 1 and not suffix: + return truncate_name( + '%s_%s' % (table_name, self._digest(column_names[0])), + self._get_connection().ops.max_name_length() + ) + + # Else generate the name for the index by South + table_name = table_name.replace('"', '').replace('.', '_') + index_unique_name = '_%x' % abs(hash((table_name, ','.join(column_names)))) + + # If the index name is too long, truncate it + index_name = ('%s_%s%s%s' % (table_name, column_names[0], index_unique_name, suffix)).replace('"', '').replace('.', '_') + if len(index_name) > self.max_index_name_length: + part = ('_%s%s%s' % (column_names[0], index_unique_name, suffix)) + index_name = '%s%s' % (table_name[:(self.max_index_name_length - len(part))], part) + + return index_name + + def create_index_sql(self, table_name, column_names, unique=False, db_tablespace=''): + """ + Generates a create index statement on 'table_name' for a list of 'column_names' + """ + if not column_names: + print "No column names supplied on which to create an index" + return '' + + connection = self._get_connection() + if db_tablespace and connection.features.supports_tablespaces: + tablespace_sql = ' ' + connection.ops.tablespace_sql(db_tablespace) + else: + tablespace_sql = '' + + index_name = self.create_index_name(table_name, column_names) + return 'CREATE %sINDEX %s ON %s (%s)%s;' % ( + unique and 'UNIQUE ' or '', + self.quote_name(index_name), + self.quote_name(table_name), + ','.join([self.quote_name(field) for field in column_names]), + tablespace_sql + ) + + @invalidate_table_constraints + def create_index(self, table_name, column_names, unique=False, db_tablespace=''): + """ Executes a create index statement """ + sql = self.create_index_sql(table_name, column_names, unique, db_tablespace) + self.execute(sql) + + @invalidate_table_constraints + def delete_index(self, table_name, column_names, db_tablespace=''): + """ + Deletes an index created with create_index. + This is possible using only columns due to the deterministic + index naming function which relies on column names. + """ + if isinstance(column_names, (str, unicode)): + column_names = [column_names] + name = self.create_index_name(table_name, column_names) + sql = self.drop_index_string % { + "index_name": self.quote_name(name), + "table_name": self.quote_name(table_name), + } + self.execute(sql) + + drop_index = alias('delete_index') + + @delete_column_constraints + def delete_column(self, table_name, name): + """ + Deletes the column 'column_name' from the table 'table_name'. + """ + params = (self.quote_name(table_name), self.quote_name(name)) + self.execute(self.delete_column_string % params, []) + + drop_column = alias('delete_column') + + def rename_column(self, table_name, old, new): + """ + Renames the column 'old' from the table 'table_name' to 'new'. + """ + raise NotImplementedError("rename_column has no generic SQL syntax") + + @invalidate_table_constraints + def delete_primary_key(self, table_name): + """ + Drops the old primary key. + """ + # Dry runs mean we can't do anything. + if self.dry_run: + if self.debug: + print ' - no dry run output for delete_primary_key() due to dynamic DDL, sorry' + return + + constraints = list(self._constraints_affecting_columns(table_name, None, type="PRIMARY KEY")) + if not constraints: + raise ValueError("Cannot find a PRIMARY KEY constraint on table %s" % (table_name,)) + + for constraint in constraints: + self.execute(self.delete_primary_key_sql % { + "table": self.quote_name(table_name), + "constraint": self.quote_name(constraint), + }) + + drop_primary_key = alias('delete_primary_key') + + @invalidate_table_constraints + def create_primary_key(self, table_name, columns): + """ + Creates a new primary key on the specified columns. + """ + if not isinstance(columns, (list, tuple)): + columns = [columns] + self.execute(self.create_primary_key_string % { + "table": self.quote_name(table_name), + "constraint": self.quote_name(table_name + "_pkey"), + "columns": ", ".join(map(self.quote_name, columns)), + }) + + def _find_primary_key_columns(self, table_name): + """ + Find all columns of the primary key of the specified table + """ + db_name = self._get_setting('NAME') + + primary_key_columns = set() + for col, constraints in self.lookup_constraint(db_name, table_name): + for kind, cname in constraints: + if kind == 'PRIMARY KEY': + primary_key_columns.add(col.lower()) + + return primary_key_columns + + def start_transaction(self): + """ + Makes sure the following commands are inside a transaction. + Must be followed by a (commit|rollback)_transaction call. + """ + if self.dry_run: + self.pending_transactions += 1 + transaction.commit_unless_managed(using=self.db_alias) + transaction.enter_transaction_management(using=self.db_alias) + transaction.managed(True, using=self.db_alias) + + def commit_transaction(self): + """ + Commits the current transaction. + Must be preceded by a start_transaction call. + """ + if self.dry_run: + return + transaction.commit(using=self.db_alias) + transaction.leave_transaction_management(using=self.db_alias) + + def rollback_transaction(self): + """ + Rolls back the current transaction. + Must be preceded by a start_transaction call. + """ + if self.dry_run: + self.pending_transactions -= 1 + transaction.rollback(using=self.db_alias) + transaction.leave_transaction_management(using=self.db_alias) + + def rollback_transactions_dry_run(self): + """ + Rolls back all pending_transactions during this dry run. + """ + if not self.dry_run: + return + while self.pending_transactions > 0: + self.rollback_transaction() + if transaction.is_dirty(using=self.db_alias): + # Force an exception, if we're still in a dirty transaction. + # This means we are missing a COMMIT/ROLLBACK. + transaction.leave_transaction_management(using=self.db_alias) + + def send_create_signal(self, app_label, model_names): + self.pending_create_signals.append((app_label, model_names)) + + def send_pending_create_signals(self, verbosity=0, interactive=False): + # Group app_labels together + signals = SortedDict() + for (app_label, model_names) in self.pending_create_signals: + try: + signals[app_label].extend(model_names) + except KeyError: + signals[app_label] = list(model_names) + # Send only one signal per app. + for (app_label, model_names) in signals.iteritems(): + self.really_send_create_signal(app_label, list(set(model_names)), + verbosity=verbosity, + interactive=interactive) + self.pending_create_signals = [] + + def really_send_create_signal(self, app_label, model_names, + verbosity=0, interactive=False): + """ + Sends a post_syncdb signal for the model specified. + + If the model is not found (perhaps it's been deleted?), + no signal is sent. + + TODO: The behavior of django.contrib.* apps seems flawed in that + they don't respect created_models. Rather, they blindly execute + over all models within the app sending the signal. This is a + patch we should push Django to make For now, this should work. + """ + + if self.debug: + print " - Sending post_syncdb signal for %s: %s" % (app_label, model_names) + + app = models.get_app(app_label) + if not app: + return + + created_models = [] + for model_name in model_names: + model = models.get_model(app_label, model_name) + if model: + created_models.append(model) + + if created_models: + + if hasattr(dispatcher, "send"): + # Older djangos + dispatcher.send(signal=models.signals.post_syncdb, sender=app, + app=app, created_models=created_models, + verbosity=verbosity, interactive=interactive) + else: + if self._is_multidb(): + # Django 1.2+ + models.signals.post_syncdb.send( + sender=app, + app=app, + created_models=created_models, + verbosity=verbosity, + interactive=interactive, + db=self.db_alias, + ) + else: + # Django 1.1 - 1.0 + models.signals.post_syncdb.send( + sender=app, + app=app, + created_models=created_models, + verbosity=verbosity, + interactive=interactive, + ) + + def mock_model(self, model_name, db_table, db_tablespace='', + pk_field_name='id', pk_field_type=models.AutoField, + pk_field_args=[], pk_field_kwargs={}): + """ + Generates a MockModel class that provides enough information + to be used by a foreign key/many-to-many relationship. + + Migrations should prefer to use these rather than actual models + as models could get deleted over time, but these can remain in + migration files forever. + + Depreciated. + """ + class MockOptions(object): + def __init__(self): + self.db_table = db_table + self.db_tablespace = db_tablespace or settings.DEFAULT_TABLESPACE + self.object_name = model_name + self.module_name = model_name.lower() + + if pk_field_type == models.AutoField: + pk_field_kwargs['primary_key'] = True + + self.pk = pk_field_type(*pk_field_args, **pk_field_kwargs) + self.pk.set_attributes_from_name(pk_field_name) + self.abstract = False + + def get_field_by_name(self, field_name): + # we only care about the pk field + return (self.pk, self.model, True, False) + + def get_field(self, name): + # we only care about the pk field + return self.pk + + class MockModel(object): + _meta = None + + # We need to return an actual class object here, not an instance + MockModel._meta = MockOptions() + MockModel._meta.model = MockModel + return MockModel + + def _db_positive_type_for_alter_column(self, klass, field): + """ + A helper for subclasses overriding _db_type_for_alter_column: + Remove the check constraint from the type string for PositiveInteger + and PositiveSmallInteger fields. + @param klass: The type of the child (required to allow this to be used when it is subclassed) + @param field: The field to generate type for + """ + super_result = super(klass, self)._db_type_for_alter_column(field) + if isinstance(field, (models.PositiveSmallIntegerField, models.PositiveIntegerField)): + return super_result.split(" ", 1)[0] + return super_result + + def _alter_add_positive_check(self, klass, field, name, params, sqls): + """ + A helper for subclasses overriding _alter_add_column_mods: + Add a check constraint verifying positivity to PositiveInteger and + PositiveSmallInteger fields. + """ + super(klass, self)._alter_add_column_mods(field, name, params, sqls) + if isinstance(field, (models.PositiveSmallIntegerField, models.PositiveIntegerField)): + uniq_hash = abs(hash(tuple(params.values()))) + d = dict( + constraint = "CK_%s_PSTV_%s" % (name, hex(uniq_hash)[2:]), + check = "%s >= 0" % self.quote_name(name)) + sqls.append((self.add_check_constraint_fragment % d, [])) + + +# Single-level flattening of lists +def flatten(ls): + nl = [] + for l in ls: + nl += l + return nl diff --git a/lib/python/south/db/generic.pyc b/lib/python/south/db/generic.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2578b80da7221968a92b9ce952e3babc93d0e552 GIT binary patch literal 40648 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 literal 0 HcmV?d00001 diff --git a/lib/python/south/db/mysql.py b/lib/python/south/db/mysql.py new file mode 100644 index 0000000..565c020 --- /dev/null +++ b/lib/python/south/db/mysql.py @@ -0,0 +1,280 @@ +# MySQL-specific implementations for south +# Original author: Andrew Godwin +# Patches by: F. Gabriel Gosselin + +from south.db import generic +from south.db.generic import DryRunError, INVALID +from south.logger import get_logger + + +def delete_column_constraints(func): + """ + Decorates column operation functions for MySQL. + Deletes the constraints from the database and clears local cache. + """ + def _column_rm(self, table_name, column_name, *args, **opts): + # Delete foreign key constraints + try: + self.delete_foreign_key(table_name, column_name) + except ValueError: + pass # If no foreign key on column, OK because it checks first + # Delete constraints referring to this column + try: + reverse = self._lookup_reverse_constraint(table_name, column_name) + for cname, rtable, rcolumn in reverse: + self.delete_foreign_key(rtable, rcolumn) + except DryRunError: + pass + return func(self, table_name, column_name, *args, **opts) + return _column_rm + + +def copy_column_constraints(func): + """ + Decorates column operation functions for MySQL. + Determines existing constraints and copies them to a new column + """ + def _column_cp(self, table_name, column_old, column_new, *args, **opts): + # Copy foreign key constraint + try: + constraint = self._find_foreign_constraints(table_name, column_old)[0] + (ftable, fcolumn) = self._lookup_constraint_references(table_name, constraint) + if ftable and fcolumn: + fk_sql = self.foreign_key_sql( + table_name, column_new, ftable, fcolumn) + get_logger().debug("Foreign key SQL: " + fk_sql) + self.add_deferred_sql(fk_sql) + except IndexError: + pass # No constraint exists so ignore + except DryRunError: + pass + # Copy constraints referring to this column + try: + reverse = self._lookup_reverse_constraint(table_name, column_old) + for cname, rtable, rcolumn in reverse: + fk_sql = self.foreign_key_sql( + rtable, rcolumn, table_name, column_new) + self.add_deferred_sql(fk_sql) + except DryRunError: + pass + return func(self, table_name, column_old, column_new, *args, **opts) + return _column_cp + + +def invalidate_table_constraints(func): + """ + For MySQL we grab all table constraints simultaneously, so this is + effective. + It further solves the issues of invalidating referred table constraints. + """ + def _cache_clear(self, table, *args, **opts): + db_name = self._get_setting('NAME') + if db_name in self._constraint_cache: + del self._constraint_cache[db_name] + if db_name in self._reverse_cache: + del self._reverse_cache[db_name] + if db_name in self._constraint_references: + del self._constraint_references[db_name] + return func(self, table, *args, **opts) + return _cache_clear + + +class DatabaseOperations(generic.DatabaseOperations): + """ + MySQL implementation of database operations. + + MySQL has no DDL transaction support This can confuse people when they ask + how to roll back - hence the dry runs, etc., found in the migration code. + """ + + backend_name = "mysql" + alter_string_set_type = '' + alter_string_set_null = 'MODIFY %(column)s %(type)s NULL;' + alter_string_drop_null = 'MODIFY %(column)s %(type)s NOT NULL;' + drop_index_string = 'DROP INDEX %(index_name)s ON %(table_name)s' + delete_primary_key_sql = "ALTER TABLE %(table)s DROP PRIMARY KEY" + delete_foreign_key_sql = "ALTER TABLE %(table)s DROP FOREIGN KEY %(constraint)s" + delete_unique_sql = "ALTER TABLE %s DROP INDEX %s" + rename_table_sql = "RENAME TABLE %s TO %s;" + + allows_combined_alters = False + has_check_constraints = False + + geom_types = ['geometry', 'point', 'linestring', 'polygon'] + text_types = ['text', 'blob'] + + def __init__(self, db_alias): + self._constraint_references = {} + self._reverse_cache = {} + super(DatabaseOperations, self).__init__(db_alias) + + def _is_valid_cache(self, db_name, table_name): + cache = self._constraint_cache + # we cache the whole db so if there are any tables table_name is valid + return db_name in cache and cache[db_name].get(table_name, None) is not INVALID + + def _fill_constraint_cache(self, db_name, table_name): + # for MySQL grab all constraints for this database. It's just as cheap as a single column. + self._constraint_cache[db_name] = {} + self._constraint_cache[db_name][table_name] = {} + self._reverse_cache[db_name] = {} + self._constraint_references[db_name] = {} + + name_query = """ + SELECT kc.`constraint_name`, kc.`column_name`, kc.`table_name`, + kc.`referenced_table_name`, kc.`referenced_column_name` + FROM information_schema.key_column_usage AS kc + WHERE + kc.table_schema = %s + """ + rows = self.execute(name_query, [db_name]) + if not rows: + return + cnames = {} + for constraint, column, table, ref_table, ref_column in rows: + key = (table, constraint) + cnames.setdefault(key, set()) + cnames[key].add((column, ref_table, ref_column)) + + type_query = """ + SELECT c.constraint_name, c.table_name, c.constraint_type + FROM information_schema.table_constraints AS c + WHERE + c.table_schema = %s + """ + rows = self.execute(type_query, [db_name]) + for constraint, table, kind in rows: + key = (table, constraint) + self._constraint_cache[db_name].setdefault(table, {}) + try: + cols = cnames[key] + except KeyError: + cols = set() + for column_set in cols: + (column, ref_table, ref_column) = column_set + self._constraint_cache[db_name][table].setdefault(column, set()) + if kind == 'FOREIGN KEY': + self._constraint_cache[db_name][table][column].add((kind, + constraint)) + # Create constraint lookup, see constraint_references + self._constraint_references[db_name][(table, + constraint)] = (ref_table, ref_column) + # Create reverse table lookup, reverse_lookup + self._reverse_cache[db_name].setdefault(ref_table, {}) + self._reverse_cache[db_name][ref_table].setdefault(ref_column, + set()) + self._reverse_cache[db_name][ref_table][ref_column].add( + (constraint, table, column)) + else: + self._constraint_cache[db_name][table][column].add((kind, + constraint)) + + def connection_init(self): + """ + Run before any SQL to let database-specific config be sent as a command, + e.g. which storage engine (MySQL) or transaction serialisability level. + """ + cursor = self._get_connection().cursor() + if self._has_setting('STORAGE_ENGINE') and self._get_setting('STORAGE_ENGINE'): + cursor.execute("SET storage_engine=%s;" % self._get_setting('STORAGE_ENGINE')) + + def start_transaction(self): + super(DatabaseOperations, self).start_transaction() + self.execute("SET FOREIGN_KEY_CHECKS=0;") + + @copy_column_constraints + @delete_column_constraints + @invalidate_table_constraints + def rename_column(self, table_name, old, new): + if old == new or self.dry_run: + return [] + + rows = [x for x in self.execute('DESCRIBE %s' % (self.quote_name(table_name),)) if x[0] == old] + + if not rows: + raise ValueError("No column '%s' in '%s'." % (old, table_name)) + + params = ( + self.quote_name(table_name), + self.quote_name(old), + self.quote_name(new), + rows[0][1], + rows[0][2] == "YES" and "NULL" or "NOT NULL", + rows[0][4] and "DEFAULT " or "", + rows[0][4] and "%s" or "", + rows[0][5] or "", + ) + + sql = 'ALTER TABLE %s CHANGE COLUMN %s %s %s %s %s %s %s;' % params + + if rows[0][4]: + self.execute(sql, (rows[0][4],)) + else: + self.execute(sql) + + @delete_column_constraints + def delete_column(self, table_name, name): + super(DatabaseOperations, self).delete_column(table_name, name) + + @invalidate_table_constraints + def rename_table(self, old_table_name, table_name): + super(DatabaseOperations, self).rename_table(old_table_name, + table_name) + + @invalidate_table_constraints + def delete_table(self, table_name): + super(DatabaseOperations, self).delete_table(table_name) + + def _lookup_constraint_references(self, table_name, cname): + """ + Provided an existing table and constraint, returns tuple of (foreign + table, column) + """ + db_name = self._get_setting('NAME') + try: + return self._constraint_references[db_name][(table_name, cname)] + except KeyError: + return None + + def _lookup_reverse_constraint(self, table_name, column_name=None): + """Look for the column referenced by a foreign constraint""" + db_name = self._get_setting('NAME') + if self.dry_run: + raise DryRunError("Cannot get constraints for columns.") + + if not self._is_valid_cache(db_name, table_name): + # Piggy-back on lookup_constraint, ensures cache exists + self.lookup_constraint(db_name, table_name) + + try: + table = self._reverse_cache[db_name][table_name] + if column_name == None: + return [(y, tuple(y)) for x, y in table.items()] + else: + return tuple(table[column_name]) + except KeyError: + return [] + + def _field_sanity(self, field): + """ + This particular override stops us sending DEFAULTs for BLOB/TEXT columns. + """ + # MySQL does not support defaults for geometry columns also + type = self._db_type_for_alter_column(field).lower() + is_geom = True in [type.find(t) > -1 for t in self.geom_types] + is_text = True in [type.find(t) > -1 for t in self.text_types] + + if is_geom or is_text: + field._suppress_default = True + return field + + def _alter_set_defaults(self, field, name, params, sqls): + """ + MySQL does not support defaults on text or blob columns. + """ + type = params['type'] + # MySQL does not support defaults for geometry columns also + is_geom = True in [type.find(t) > -1 for t in self.geom_types] + is_text = True in [type.find(t) > -1 for t in self.text_types] + if not is_geom and not is_text: + super(DatabaseOperations, self)._alter_set_defaults(field, name, params, sqls) diff --git a/lib/python/south/db/mysql.pyc b/lib/python/south/db/mysql.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e69b93cabbb4f8b55e3b64a22c813db7aa582507 GIT binary patch literal 10703 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 literal 0 HcmV?d00001 diff --git a/lib/python/south/db/oracle.py b/lib/python/south/db/oracle.py new file mode 100644 index 0000000..ed5e87f --- /dev/null +++ b/lib/python/south/db/oracle.py @@ -0,0 +1,299 @@ +import os.path +import sys +import re +import warnings +import cx_Oracle + + +from django.db import connection, models +from django.db.backends.util import truncate_name +from django.core.management.color import no_style +from django.db.models.fields import NOT_PROVIDED +from django.db.utils import DatabaseError + +# In revision r16016 function get_sequence_name has been transformed into +# method of DatabaseOperations class. To make code backward-compatible we +# need to handle both situations. +try: + from django.db.backends.oracle.base import get_sequence_name\ + as original_get_sequence_name +except ImportError: + original_get_sequence_name = None + +from south.db import generic + +warnings.warn("! WARNING: South's Oracle support is still alpha. " + "Be wary of possible bugs.") + +class DatabaseOperations(generic.DatabaseOperations): + """ + Oracle implementation of database operations. + """ + backend_name = 'oracle' + + alter_string_set_type = 'ALTER TABLE %(table_name)s MODIFY %(column)s %(type)s %(nullity)s;' + alter_string_set_default = 'ALTER TABLE %(table_name)s MODIFY %(column)s DEFAULT %(default)s;' + add_column_string = 'ALTER TABLE %s ADD %s;' + delete_column_string = 'ALTER TABLE %s DROP COLUMN %s;' + add_constraint_string = 'ALTER TABLE %(table_name)s ADD CONSTRAINT %(constraint)s %(clause)s' + + allows_combined_alters = False + has_booleans = False + + constraints_dict = { + 'P': 'PRIMARY KEY', + 'U': 'UNIQUE', + 'C': 'CHECK', + 'R': 'FOREIGN KEY' + } + + def get_sequence_name(self, table_name): + if original_get_sequence_name is None: + return self._get_connection().ops._get_sequence_name(table_name) + else: + return original_get_sequence_name(table_name) + + #TODO: This will cause very obscure bugs if anyone uses a column name or string value + # that looks like a column definition (with 'CHECK', 'DEFAULT' and/or 'NULL' in it) + # e.g. "CHECK MATE" varchar(10) DEFAULT 'NULL' + def adj_column_sql(self, col): + # Syntax fixes -- Oracle is picky about clause order + col = re.sub('(?PCHECK \(.*\))(?P.*)(?PDEFAULT \d+)', + lambda mo: '%s %s%s'%(mo.group('default'), mo.group('constr'), mo.group('any')), col) #syntax fix for boolean/integer field only + col = re.sub('(?P(NOT )?NULL) (?P(.* )?)(?PDEFAULT.+)', + lambda mo: '%s %s %s'%(mo.group('default'),mo.group('not_null'),mo.group('misc') or ''), col) #fix order of NULL/NOT NULL and DEFAULT + return col + + def check_meta(self, table_name): + return table_name in [ m._meta.db_table for m in models.get_models() ] #caching provided by Django + + def normalize_name(self, name): + """ + Get the properly shortened and uppercased identifier as returned by quote_name(), but without the actual quotes. + """ + nn = self.quote_name(name) + if nn[0] == '"' and nn[-1] == '"': + nn = nn[1:-1] + return nn + + @generic.invalidate_table_constraints + def create_table(self, table_name, fields): + qn = self.quote_name(table_name) + columns = [] + autoinc_sql = '' + + for field_name, field in fields: + col = self.column_sql(table_name, field_name, field) + if not col: + continue + col = self.adj_column_sql(col) + + columns.append(col) + if isinstance(field, models.AutoField): + autoinc_sql = connection.ops.autoinc_sql(table_name, field_name) + + sql = 'CREATE TABLE %s (%s);' % (qn, ', '.join([col for col in columns])) + self.execute(sql) + if autoinc_sql: + self.execute(autoinc_sql[0]) + self.execute(autoinc_sql[1]) + + @generic.invalidate_table_constraints + def delete_table(self, table_name, cascade=True): + qn = self.quote_name(table_name) + + # Note: PURGE is not valid syntax for Oracle 9i (it was added in 10) + if cascade: + self.execute('DROP TABLE %s CASCADE CONSTRAINTS;' % qn) + else: + self.execute('DROP TABLE %s;' % qn) + + # If the table has an AutoField a sequence was created. + sequence_sql = """ +DECLARE + i INTEGER; +BEGIN + SELECT COUNT(*) INTO i FROM USER_CATALOG + WHERE TABLE_NAME = '%(sq_name)s' AND TABLE_TYPE = 'SEQUENCE'; + IF i = 1 THEN + EXECUTE IMMEDIATE 'DROP SEQUENCE "%(sq_name)s"'; + END IF; +END; +/""" % {'sq_name': self.get_sequence_name(table_name)} + self.execute(sequence_sql) + + @generic.invalidate_table_constraints + def alter_column(self, table_name, name, field, explicit_name=True): + + if self.dry_run: + if self.debug: + print ' - no dry run output for alter_column() due to dynamic DDL, sorry' + return + + qn = self.quote_name(table_name) + + # hook for the field to do any resolution prior to it's attributes being queried + if hasattr(field, 'south_init'): + field.south_init() + field = self._field_sanity(field) + + # Add _id or whatever if we need to + field.set_attributes_from_name(name) + if not explicit_name: + name = field.column + qn_col = self.quote_name(name) + + # First, change the type + # This will actually also add any CHECK constraints needed, + # since e.g. 'type' for a BooleanField is 'NUMBER(1) CHECK (%(qn_column)s IN (0,1))' + params = { + 'table_name':qn, + 'column': qn_col, + 'type': self._db_type_for_alter_column(field), + 'nullity': 'NOT NULL', + 'default': 'NULL' + } + if field.null: + params['nullity'] = 'NULL' + + if not field.null and field.has_default(): + params['default'] = self._default_value_workaround(field.get_default()) + + sql_templates = [ + (self.alter_string_set_type, params), + (self.alter_string_set_default, params.copy()), + ] + + # drop CHECK constraints. Make sure this is executed before the ALTER TABLE statements + # generated above, since those statements recreate the constraints we delete here. + check_constraints = self._constraints_affecting_columns(table_name, [name], "CHECK") + for constraint in check_constraints: + self.execute(self.delete_check_sql % { + 'table': self.quote_name(table_name), + 'constraint': self.quote_name(constraint), + }) + + for sql_template, params in sql_templates: + try: + self.execute(sql_template % params) + except DatabaseError, exc: + description = str(exc) + # Oracle complains if a column is already NULL/NOT NULL + if 'ORA-01442' in description or 'ORA-01451' in description: + # so we just drop NULL/NOT NULL part from target sql and retry + params['nullity'] = '' + sql = sql_template % params + self.execute(sql) + # Oracle also has issues if we try to change a regular column + # to a LOB or vice versa (also REF, object, VARRAY or nested + # table, but these don't come up much in Django apps) + elif 'ORA-22858' in description or 'ORA-22859' in description: + self._alter_column_lob_workaround(table_name, name, field) + else: + raise + + def _alter_column_lob_workaround(self, table_name, name, field): + """ + Oracle refuses to change a column type from/to LOB to/from a regular + column. In Django, this shows up when the field is changed from/to + a TextField. + What we need to do instead is: + - Rename the original column + - Add the desired field as new + - Update the table to transfer values from old to new + - Drop old column + """ + renamed = self._generate_temp_name(name) + self.rename_column(table_name, name, renamed) + self.add_column(table_name, name, field, keep_default=False) + self.execute("UPDATE %s set %s=%s" % ( + self.quote_name(table_name), + self.quote_name(name), + self.quote_name(renamed), + )) + self.delete_column(table_name, renamed) + + def _generate_temp_name(self, for_name): + suffix = hex(hash(for_name)).upper()[1:] + return self.normalize_name(for_name + "_" + suffix) + + @generic.copy_column_constraints #TODO: Appears to be nulled by the delete decorator below... + @generic.delete_column_constraints + def rename_column(self, table_name, old, new): + if old == new: + # Short-circuit out + return [] + self.execute('ALTER TABLE %s RENAME COLUMN %s TO %s;' % ( + self.quote_name(table_name), + self.quote_name(old), + self.quote_name(new), + )) + + @generic.invalidate_table_constraints + def add_column(self, table_name, name, field, keep_default=True): + sql = self.column_sql(table_name, name, field) + sql = self.adj_column_sql(sql) + + if sql: + params = ( + self.quote_name(table_name), + sql + ) + sql = self.add_column_string % params + self.execute(sql) + + # Now, drop the default if we need to + if not keep_default and field.default is not None: + field.default = NOT_PROVIDED + self.alter_column(table_name, name, field, explicit_name=False) + + def delete_column(self, table_name, name): + return super(DatabaseOperations, self).delete_column(self.quote_name(table_name), name) + + def lookup_constraint(self, db_name, table_name, column_name=None): + if column_name: + # Column names in the constraint cache come from the database, + # make sure we use the properly shortened/uppercased version + # for lookup. + column_name = self.normalize_name(column_name) + return super(DatabaseOperations, self).lookup_constraint(db_name, table_name, column_name) + + def _constraints_affecting_columns(self, table_name, columns, type="UNIQUE"): + if columns: + columns = [self.normalize_name(c) for c in columns] + return super(DatabaseOperations, self)._constraints_affecting_columns(table_name, columns, type) + + def _field_sanity(self, field): + """ + This particular override stops us sending DEFAULTs for BooleanField. + """ + if isinstance(field, models.BooleanField) and field.has_default(): + field.default = int(field.to_python(field.get_default())) + return field + + def _default_value_workaround(self, value): + from datetime import date,time,datetime + if isinstance(value, (date,time,datetime)): + return "'%s'" % value + else: + return super(DatabaseOperations, self)._default_value_workaround(value) + + def _fill_constraint_cache(self, db_name, table_name): + self._constraint_cache.setdefault(db_name, {}) + self._constraint_cache[db_name][table_name] = {} + + rows = self.execute(""" + SELECT user_cons_columns.constraint_name, + user_cons_columns.column_name, + user_constraints.constraint_type + FROM user_constraints + JOIN user_cons_columns ON + user_constraints.table_name = user_cons_columns.table_name AND + user_constraints.constraint_name = user_cons_columns.constraint_name + WHERE user_constraints.table_name = '%s' + """ % self.normalize_name(table_name)) + + for constraint, column, kind in rows: + self._constraint_cache[db_name][table_name].setdefault(column, set()) + self._constraint_cache[db_name][table_name][column].add((self.constraints_dict[kind], constraint)) + return diff --git a/lib/python/south/db/oracle.pyc b/lib/python/south/db/oracle.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f0a06fd06badc237079a50cc655e69382464d2e GIT binary patch literal 11096 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 a#K-Fa)o=ifv&qd*Da}c>16f)O#0&uHNG25k literal 0 HcmV?d00001 diff --git a/lib/python/south/db/sql_server/pyodbc.py b/lib/python/south/db/sql_server/pyodbc.py new file mode 100644 index 0000000..39e2127 --- /dev/null +++ b/lib/python/south/db/sql_server/pyodbc.py @@ -0,0 +1,434 @@ +from datetime import date, datetime, time +from warnings import warn +from django.db import models +from django.db.models import fields +from south.db import generic +from south.db.generic import delete_column_constraints, invalidate_table_constraints, copy_column_constraints +from south.exceptions import ConstraintDropped +from django.utils.encoding import smart_unicode +from django.core.management.color import no_style + +class DatabaseOperations(generic.DatabaseOperations): + """ + django-pyodbc (sql_server.pyodbc) implementation of database operations. + """ + + backend_name = "pyodbc" + + add_column_string = 'ALTER TABLE %s ADD %s;' + alter_string_set_type = 'ALTER COLUMN %(column)s %(type)s' + alter_string_set_null = 'ALTER COLUMN %(column)s %(type)s NULL' + alter_string_drop_null = 'ALTER COLUMN %(column)s %(type)s NOT NULL' + + allows_combined_alters = False + + drop_index_string = 'DROP INDEX %(index_name)s ON %(table_name)s' + drop_constraint_string = 'ALTER TABLE %(table_name)s DROP CONSTRAINT %(constraint_name)s' + delete_column_string = 'ALTER TABLE %s DROP COLUMN %s' + + #create_check_constraint_sql = "ALTER TABLE %(table)s " + \ + # generic.DatabaseOperations.add_check_constraint_fragment + create_foreign_key_sql = "ALTER TABLE %(table)s ADD CONSTRAINT %(constraint)s " + \ + "FOREIGN KEY (%(column)s) REFERENCES %(target)s" + create_unique_sql = "ALTER TABLE %(table)s ADD CONSTRAINT %(constraint)s UNIQUE (%(columns)s)" + + + default_schema_name = "dbo" + + has_booleans = False + + + @delete_column_constraints + def delete_column(self, table_name, name): + q_table_name, q_name = (self.quote_name(table_name), self.quote_name(name)) + + # Zap the indexes + for ind in self._find_indexes_for_column(table_name,name): + params = {'table_name':q_table_name, 'index_name': ind} + sql = self.drop_index_string % params + self.execute(sql, []) + + # Zap the constraints + for const in self._find_constraints_for_column(table_name,name): + params = {'table_name':q_table_name, 'constraint_name': const} + sql = self.drop_constraint_string % params + self.execute(sql, []) + + # Zap default if exists + drop_default = self.drop_column_default_sql(table_name, name) + if drop_default: + sql = "ALTER TABLE [%s] %s" % (table_name, drop_default) + self.execute(sql, []) + + # Finally zap the column itself + self.execute(self.delete_column_string % (q_table_name, q_name), []) + + def _find_indexes_for_column(self, table_name, name): + "Find the indexes that apply to a column, needed when deleting" + + sql = """ + SELECT si.name, si.id, sik.colid, sc.name + FROM dbo.sysindexes SI WITH (NOLOCK) + INNER JOIN dbo.sysindexkeys SIK WITH (NOLOCK) + ON SIK.id = Si.id + AND SIK.indid = SI.indid + INNER JOIN dbo.syscolumns SC WITH (NOLOCK) + ON SI.id = SC.id + AND SIK.colid = SC.colid + WHERE SI.indid !=0 + AND Si.id = OBJECT_ID('%s') + AND SC.name = '%s' + """ + idx = self.execute(sql % (table_name, name), []) + return [i[0] for i in idx] + + + def _find_constraints_for_column(self, table_name, name, just_names=True): + """ + Find the constraints that apply to a column, needed when deleting. Defaults not included. + This is more general than the parent _constraints_affecting_columns, as on MSSQL this + includes PK and FK constraints. + """ + + sql = """ + SELECT CC.[CONSTRAINT_NAME] + ,TC.[CONSTRAINT_TYPE] + ,CHK.[CHECK_CLAUSE] + ,RFD.TABLE_SCHEMA + ,RFD.TABLE_NAME + ,RFD.COLUMN_NAME + -- used for normalized names + ,CC.TABLE_NAME + ,CC.COLUMN_NAME + FROM [INFORMATION_SCHEMA].[TABLE_CONSTRAINTS] TC + JOIN INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE CC + ON TC.CONSTRAINT_CATALOG = CC.CONSTRAINT_CATALOG + AND TC.CONSTRAINT_SCHEMA = CC.CONSTRAINT_SCHEMA + AND TC.CONSTRAINT_NAME = CC.CONSTRAINT_NAME + LEFT JOIN INFORMATION_SCHEMA.CHECK_CONSTRAINTS CHK + ON CHK.CONSTRAINT_CATALOG = CC.CONSTRAINT_CATALOG + AND CHK.CONSTRAINT_SCHEMA = CC.CONSTRAINT_SCHEMA + AND CHK.CONSTRAINT_NAME = CC.CONSTRAINT_NAME + AND 'CHECK' = TC.CONSTRAINT_TYPE + LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS REF + ON REF.CONSTRAINT_CATALOG = CC.CONSTRAINT_CATALOG + AND REF.CONSTRAINT_SCHEMA = CC.CONSTRAINT_SCHEMA + AND REF.CONSTRAINT_NAME = CC.CONSTRAINT_NAME + AND 'FOREIGN KEY' = TC.CONSTRAINT_TYPE + LEFT JOIN INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE RFD + ON RFD.CONSTRAINT_CATALOG = REF.UNIQUE_CONSTRAINT_CATALOG + AND RFD.CONSTRAINT_SCHEMA = REF.UNIQUE_CONSTRAINT_SCHEMA + AND RFD.CONSTRAINT_NAME = REF.UNIQUE_CONSTRAINT_NAME + WHERE CC.CONSTRAINT_CATALOG = CC.TABLE_CATALOG + AND CC.CONSTRAINT_SCHEMA = CC.TABLE_SCHEMA + AND CC.TABLE_CATALOG = %s + AND CC.TABLE_SCHEMA = %s + AND CC.TABLE_NAME = %s + AND CC.COLUMN_NAME = %s + """ + db_name = self._get_setting('name') + schema_name = self._get_schema_name() + table = self.execute(sql, [db_name, schema_name, table_name, name]) + + if just_names: + return [r[0] for r in table] + + all = {} + for r in table: + cons_name, type = r[:2] + if type=='PRIMARY KEY' or type=='UNIQUE': + cons = all.setdefault(cons_name, (type,[])) + cons[1].append(r[7]) + elif type=='CHECK': + cons = (type, r[2]) + elif type=='FOREIGN KEY': + if cons_name in all: + raise NotImplementedError("Multiple-column foreign keys are not supported") + else: + cons = (type, r[3:6]) + else: + raise NotImplementedError("Don't know how to handle constraints of type "+ type) + all[cons_name] = cons + return all + + @invalidate_table_constraints + def alter_column(self, table_name, name, field, explicit_name=True, ignore_constraints=False): + """ + Alters the given column name so it will match the given field. + Note that conversion between the two by the database must be possible. + Will not automatically add _id by default; to have this behavour, pass + explicit_name=False. + + @param table_name: The name of the table to add the column to + @param name: The name of the column to alter + @param field: The new field definition to use + """ + self._fix_field_definition(field) + + if not ignore_constraints: + qn = self.quote_name + sch = qn(self._get_schema_name()) + tab = qn(table_name) + table = ".".join([sch, tab]) + try: + self.delete_foreign_key(table_name, name) + except ValueError: + # no FK constraint on this field. That's OK. + pass + constraints = self._find_constraints_for_column(table_name, name, False) + for constraint in constraints.keys(): + params = dict(table_name = table, + constraint_name = qn(constraint)) + sql = self.drop_constraint_string % params + self.execute(sql, []) + + ret_val = super(DatabaseOperations, self).alter_column(table_name, name, field, explicit_name, ignore_constraints=True) + + if not ignore_constraints: + for cname, (ctype,args) in constraints.items(): + params = dict(table = table, + constraint = qn(cname)) + if ctype=='UNIQUE': + params['columns'] = ", ".join(map(qn,args)) + sql = self.create_unique_sql % params + elif ctype=='PRIMARY KEY': + params['columns'] = ", ".join(map(qn,args)) + sql = self.create_primary_key_string % params + elif ctype=='FOREIGN KEY': + continue + # Foreign keys taken care of below + #target = "%s.%s(%s)" % tuple(map(qn,args)) + #params.update(column = qn(name), target = target) + #sql = self.create_foreign_key_sql % params + elif ctype=='CHECK': + warn(ConstraintDropped("CHECK "+ args, table_name, name)) + continue + #TODO: Some check constraints should be restored; but not before the generic + # backend restores them. + #params['check'] = args + #sql = self.create_check_constraint_sql % params + else: + raise NotImplementedError("Don't know how to handle constraints of type "+ type) + self.execute(sql, []) + # Create foreign key if necessary + if field.rel and self.supports_foreign_keys: + self.execute( + self.foreign_key_sql( + table_name, + field.column, + field.rel.to._meta.db_table, + field.rel.to._meta.get_field(field.rel.field_name).column + ) + ) + model = self.mock_model("FakeModelForIndexCreation", table_name) + for stmt in self._get_connection().creation.sql_indexes_for_field(model, field, no_style()): + self.execute(stmt) + + + return ret_val + + def _alter_set_defaults(self, field, name, params, sqls): + "Subcommand of alter_column that sets default values (overrideable)" + # First drop the current default if one exists + table_name = self.quote_name(params['table_name']) + drop_default = self.drop_column_default_sql(table_name, name) + if drop_default: + sqls.append((drop_default, [])) + + # Next, set any default + + if field.has_default(): + default = field.get_default() + literal = self._value_to_unquoted_literal(field, default) + sqls.append(('ADD DEFAULT %s for %s' % (self._quote_string(literal), self.quote_name(name),), [])) + + def _value_to_unquoted_literal(self, field, value): + # Start with the field's own translation + conn = self._get_connection() + value = field.get_db_prep_save(value, connection=conn) + # This is still a Python object -- nobody expects to need a literal. + if isinstance(value, basestring): + return smart_unicode(value) + elif isinstance(value, (date,time,datetime)): + return value.isoformat() + else: + #TODO: Anybody else needs special translations? + return str(value) + def _default_value_workaround(self, value): + if isinstance(value, (date,time,datetime)): + return value.isoformat() + else: + return super(DatabaseOperations, self)._default_value_workaround(value) + + def _quote_string(self, s): + return "'" + s.replace("'","''") + "'" + + + def drop_column_default_sql(self, table_name, name, q_name=None): + "MSSQL specific drop default, which is a pain" + + sql = """ + SELECT object_name(cdefault) + FROM syscolumns + WHERE id = object_id('%s') + AND name = '%s' + """ + cons = self.execute(sql % (table_name, name), []) + if cons and cons[0] and cons[0][0]: + return "DROP CONSTRAINT %s" % cons[0][0] + return None + + def _fix_field_definition(self, field): + if isinstance(field, (fields.BooleanField, fields.NullBooleanField)): + if field.default == True: + field.default = 1 + if field.default == False: + field.default = 0 + + # This is copied from South's generic add_column, with two modifications: + # 1) The sql-server-specific call to _fix_field_definition + # 2) Removing a default, when needed, by calling drop_default and not the more general alter_column + @invalidate_table_constraints + def add_column(self, table_name, name, field, keep_default=True): + """ + Adds the column 'name' to the table 'table_name'. + Uses the 'field' paramater, a django.db.models.fields.Field instance, + to generate the necessary sql + + @param table_name: The name of the table to add the column to + @param name: The name of the column to add + @param field: The field to use + """ + self._fix_field_definition(field) + sql = self.column_sql(table_name, name, field) + if sql: + params = ( + self.quote_name(table_name), + sql, + ) + sql = self.add_column_string % params + self.execute(sql) + + # Now, drop the default if we need to + if not keep_default and field.default is not None: + field.default = fields.NOT_PROVIDED + #self.alter_column(table_name, name, field, explicit_name=False, ignore_constraints=True) + self.drop_default(table_name, name, field) + + @invalidate_table_constraints + def drop_default(self, table_name, name, field): + fragment = self.drop_column_default_sql(table_name, name) + if fragment: + table_name = self.quote_name(table_name) + sql = " ".join(["ALTER TABLE", table_name, fragment]) + self.execute(sql) + + + @invalidate_table_constraints + def create_table(self, table_name, field_defs): + # Tweak stuff as needed + for _, f in field_defs: + self._fix_field_definition(f) + + # Run + generic.DatabaseOperations.create_table(self, table_name, field_defs) + + def _find_referencing_fks(self, table_name): + "MSSQL does not support cascading FKs when dropping tables, we need to implement." + + # FK -- Foreign Keys + # UCTU -- Unique Constraints Table Usage + # FKTU -- Foreign Key Table Usage + # (last two are both really CONSTRAINT_TABLE_USAGE, different join conditions) + sql = """ + SELECT FKTU.TABLE_SCHEMA as REFING_TABLE_SCHEMA, + FKTU.TABLE_NAME as REFING_TABLE_NAME, + FK.[CONSTRAINT_NAME] as FK_NAME + FROM [INFORMATION_SCHEMA].[REFERENTIAL_CONSTRAINTS] FK + JOIN [INFORMATION_SCHEMA].[CONSTRAINT_TABLE_USAGE] UCTU + ON FK.UNIQUE_CONSTRAINT_CATALOG = UCTU.CONSTRAINT_CATALOG and + FK.UNIQUE_CONSTRAINT_NAME = UCTU.CONSTRAINT_NAME and + FK.UNIQUE_CONSTRAINT_SCHEMA = UCTU.CONSTRAINT_SCHEMA + JOIN [INFORMATION_SCHEMA].[CONSTRAINT_TABLE_USAGE] FKTU + ON FK.CONSTRAINT_CATALOG = FKTU.CONSTRAINT_CATALOG and + FK.CONSTRAINT_NAME = FKTU.CONSTRAINT_NAME and + FK.CONSTRAINT_SCHEMA = FKTU.CONSTRAINT_SCHEMA + WHERE FK.CONSTRAINT_CATALOG = %s + AND UCTU.TABLE_SCHEMA = %s -- REFD_TABLE_SCHEMA + AND UCTU.TABLE_NAME = %s -- REFD_TABLE_NAME + """ + db_name = self._get_setting('name') + schema_name = self._get_schema_name() + return self.execute(sql, [db_name, schema_name, table_name]) + + @invalidate_table_constraints + def delete_table(self, table_name, cascade=True): + """ + Deletes the table 'table_name'. + """ + if cascade: + refing = self._find_referencing_fks(table_name) + for schmea, table, constraint in refing: + table = ".".join(map (self.quote_name, [schmea, table])) + params = dict(table_name = table, + constraint_name = self.quote_name(constraint)) + sql = self.drop_constraint_string % params + self.execute(sql, []) + cascade = False + super(DatabaseOperations, self).delete_table(table_name, cascade) + + @copy_column_constraints + @delete_column_constraints + def rename_column(self, table_name, old, new): + """ + Renames the column of 'table_name' from 'old' to 'new'. + WARNING - This isn't transactional on MSSQL! + """ + if old == new: + # No Operation + return + # Examples on the MS site show the table name not being quoted... + params = (table_name, self.quote_name(old), self.quote_name(new)) + self.execute("EXEC sp_rename '%s.%s', %s, 'COLUMN'" % params) + + @invalidate_table_constraints + def rename_table(self, old_table_name, table_name): + """ + Renames the table 'old_table_name' to 'table_name'. + WARNING - This isn't transactional on MSSQL! + """ + if old_table_name == table_name: + # No Operation + return + params = (self.quote_name(old_table_name), self.quote_name(table_name)) + self.execute('EXEC sp_rename %s, %s' % params) + + def _db_type_for_alter_column(self, field): + return self._db_positive_type_for_alter_column(DatabaseOperations, field) + + def _alter_add_column_mods(self, field, name, params, sqls): + return self._alter_add_positive_check(DatabaseOperations, field, name, params, sqls) + + @invalidate_table_constraints + def delete_foreign_key(self, table_name, column): + super(DatabaseOperations, self).delete_foreign_key(table_name, column) + # A FK also implies a non-unique index + find_index_sql = """ + SELECT i.name -- s.name, t.name, c.name + FROM sys.tables t + INNER JOIN sys.schemas s ON t.schema_id = s.schema_id + INNER JOIN sys.indexes i ON i.object_id = t.object_id + INNER JOIN sys.index_columns ic ON ic.object_id = t.object_id + INNER JOIN sys.columns c ON c.object_id = t.object_id + AND ic.column_id = c.column_id + WHERE i.is_unique=0 AND i.is_primary_key=0 AND i.is_unique_constraint=0 + AND s.name = %s + AND t.name = %s + AND c.name = %s + """ + schema = self._get_schema_name() + indexes = self.execute(find_index_sql, [schema, table_name, column]) + qn = self.quote_name + for index in (i[0] for i in indexes if i[0]): # "if i[0]" added because an empty name may return + self.execute("DROP INDEX %s on %s.%s" % (qn(index), qn(schema), qn(table_name) )) + diff --git a/lib/python/south/db/sql_server/pyodbc.pyc b/lib/python/south/db/sql_server/pyodbc.pyc new file mode 100644 index 0000000000000000000000000000000000000000..938e4038084a16cdbe7c70edddc6adec483a5369 GIT binary patch literal 17251 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 literal 0 HcmV?d00001 diff --git a/lib/python/south/db/sqlite3.py b/lib/python/south/db/sqlite3.py new file mode 100644 index 0000000..02682ad --- /dev/null +++ b/lib/python/south/db/sqlite3.py @@ -0,0 +1,252 @@ +from south.db import generic + + +class DatabaseOperations(generic.DatabaseOperations): + + """ + SQLite3 implementation of database operations. + """ + + backend_name = "sqlite3" + + # SQLite ignores several constraints. I wish I could. + supports_foreign_keys = False + has_check_constraints = False + has_booleans = False + + def add_column(self, table_name, name, field, *args, **kwds): + """ + Adds a column. + """ + # If it's not nullable, and has no default, raise an error (SQLite is picky) + if (not field.null and + (not field.has_default() or field.get_default() is None) and + not field.empty_strings_allowed): + raise ValueError("You cannot add a null=False column without a default value.") + # Initialise the field. + field.set_attributes_from_name(name) + # We add columns by remaking the table; even though SQLite supports + # adding columns, it doesn't support adding PRIMARY KEY or UNIQUE cols. + self._remake_table(table_name, added={ + field.column: self._column_sql_for_create(table_name, name, field, False), + }) + + def _get_full_table_description(self, connection, cursor, table_name): + cursor.execute('PRAGMA table_info(%s)' % connection.ops.quote_name(table_name)) + # cid, name, type, notnull, dflt_value, pk + return [{'name': field[1], + 'type': field[2], + 'null_ok': not field[3], + 'dflt_value': field[4], + 'pk': field[5] # undocumented + } for field in cursor.fetchall()] + + @generic.invalidate_table_constraints + def _remake_table(self, table_name, added={}, renames={}, deleted=[], altered={}, primary_key_override=None, uniques_deleted=[]): + """ + Given a table and three sets of changes (renames, deletes, alters), + recreates it with the modified schema. + """ + # Dry runs get skipped completely + if self.dry_run: + return + # Temporary table's name + temp_name = "_south_new_" + table_name + # Work out the (possibly new) definitions of each column + definitions = {} + cursor = self._get_connection().cursor() + # Get the index descriptions + indexes = self._get_connection().introspection.get_indexes(cursor, table_name) + multi_indexes = self._get_multi_indexes(table_name) + # Work out new column defs. + for column_info in self._get_full_table_description(self._get_connection(), cursor, table_name): + name = column_info['name'] + if name in deleted: + continue + # Get the type, ignoring PRIMARY KEY (we need to be consistent) + type = column_info['type'].replace("PRIMARY KEY", "") + # Add on primary key, not null or unique if needed. + if (primary_key_override and primary_key_override == name) or \ + (not primary_key_override and name in indexes and + indexes[name]['primary_key']): + type += " PRIMARY KEY" + elif not column_info['null_ok']: + type += " NOT NULL" + if (name in indexes and indexes[name]['unique'] and + name not in uniques_deleted): + type += " UNIQUE" + if column_info['dflt_value'] is not None: + type += " DEFAULT " + column_info['dflt_value'] + # Deal with a rename + if name in renames: + name = renames[name] + # Add to the defs + definitions[name] = type + # Add on altered columns + for name, type in altered.items(): + if (primary_key_override and primary_key_override == name) or \ + (not primary_key_override and name in indexes and + indexes[name]['primary_key']): + type += " PRIMARY KEY" + if (name in indexes and indexes[name]['unique'] and + name not in uniques_deleted): + type += " UNIQUE" + definitions[name] = type + # Add on the new columns + for name, type in added.items(): + if (primary_key_override and primary_key_override == name): + type += " PRIMARY KEY" + definitions[name] = type + # Alright, Make the table + self.execute("CREATE TABLE %s (%s)" % ( + self.quote_name(temp_name), + ", ".join(["%s %s" % (self.quote_name(cname), ctype) for cname, ctype in definitions.items()]), + )) + # Copy over the data + self._copy_data(table_name, temp_name, renames) + # Delete the old table, move our new one over it + self.delete_table(table_name) + self.rename_table(temp_name, table_name) + # Recreate multi-valued indexes + # We can't do that before since it's impossible to rename indexes + # and index name scope is global + self._make_multi_indexes(table_name, multi_indexes, renames=renames, deleted=deleted, uniques_deleted=uniques_deleted) + + def _copy_data(self, src, dst, field_renames={}): + "Used to copy data into a new table" + # Make a list of all the fields to select + cursor = self._get_connection().cursor() + src_fields = [column_info[0] for column_info in self._get_connection().introspection.get_table_description(cursor, src)] + dst_fields = [column_info[0] for column_info in self._get_connection().introspection.get_table_description(cursor, dst)] + src_fields_new = [] + dst_fields_new = [] + for field in src_fields: + if field in field_renames: + dst_fields_new.append(self.quote_name(field_renames[field])) + elif field in dst_fields: + dst_fields_new.append(self.quote_name(field)) + else: + continue + src_fields_new.append(self.quote_name(field)) + # Copy over the data + self.execute("INSERT INTO %s (%s) SELECT %s FROM %s;" % ( + self.quote_name(dst), + ', '.join(dst_fields_new), + ', '.join(src_fields_new), + self.quote_name(src), + )) + + def _create_unique(self, table_name, columns): + self.execute("CREATE UNIQUE INDEX %s ON %s(%s);" % ( + self.quote_name('%s_%s' % (table_name, '__'.join(columns))), + self.quote_name(table_name), + ', '.join(self.quote_name(c) for c in columns), + )) + + def _get_multi_indexes(self, table_name): + indexes = [] + cursor = self._get_connection().cursor() + cursor.execute('PRAGMA index_list(%s)' % self.quote_name(table_name)) + # seq, name, unique + for index, unique in [(field[1], field[2]) for field in cursor.fetchall()]: + if not unique: + continue + cursor.execute('PRAGMA index_info(%s)' % self.quote_name(index)) + info = cursor.fetchall() + if len(info) == 1: + continue + columns = [] + for field in info: + columns.append(field[2]) + indexes.append(columns) + return indexes + + def _make_multi_indexes(self, table_name, indexes, deleted=[], renames={}, uniques_deleted=[]): + for index in indexes: + columns = [] + + for name in index: + # Handle deletion + if name in deleted: + columns = [] + break + + # Handle renames + if name in renames: + name = renames[name] + columns.append(name) + + if columns and columns != uniques_deleted: + self._create_unique(table_name, columns) + + def _column_sql_for_create(self, table_name, name, field, explicit_name=True): + "Given a field and its name, returns the full type for the CREATE TABLE (without unique/pk)" + field.set_attributes_from_name(name) + if not explicit_name: + name = field.db_column + else: + field.column = name + sql = self.column_sql(table_name, name, field, with_name=False, field_prepared=True) + # Remove keywords we don't want (this should be type only, not constraint) + if sql: + sql = sql.replace("PRIMARY KEY", "") + return sql + + def alter_column(self, table_name, name, field, explicit_name=True, ignore_constraints=False): + """ + Changes a column's SQL definition. + + Note that this sqlite3 implementation ignores the ignore_constraints argument. + The argument is accepted for API compatibility with the generic + DatabaseOperations.alter_column() method. + """ + # Remake the table correctly + self._remake_table(table_name, altered={ + name: self._column_sql_for_create(table_name, name, field, explicit_name), + }) + + def delete_column(self, table_name, column_name): + """ + Deletes a column. + """ + self._remake_table(table_name, deleted=[column_name]) + + def rename_column(self, table_name, old, new): + """ + Renames a column from one name to another. + """ + self._remake_table(table_name, renames={old: new}) + + def create_unique(self, table_name, columns): + """ + Create an unique index on columns + """ + self._create_unique(table_name, columns) + + def delete_unique(self, table_name, columns): + """ + Delete an unique index + """ + self._remake_table(table_name, uniques_deleted=columns) + + def create_primary_key(self, table_name, columns): + if not isinstance(columns, (list, tuple)): + columns = [columns] + assert len(columns) == 1, "SQLite backend does not support multi-column primary keys" + self._remake_table(table_name, primary_key_override=columns[0]) + + # Not implemented this yet. + def delete_primary_key(self, table_name): + # By passing True in, we make sure we wipe all existing PKs. + self._remake_table(table_name, primary_key_override=True) + + # No cascades on deletes + def delete_table(self, table_name, cascade=True): + generic.DatabaseOperations.delete_table(self, table_name, False) + + def _default_value_workaround(self, default): + if default == True: + default = 1 + elif default == False: + default = 0 + return default diff --git a/lib/python/south/db/sqlite3.pyc b/lib/python/south/db/sqlite3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..953eb2de7759553dba41d82c3e73f518aec25c1c GIT binary patch literal 9158 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 VEOricj&zpswsI zL;cuU=#+ksun1;!7OdB*kE2s$BPi#9QbiaBoH0us`g!D_2zsa1ute(M=1uu|sL_)h zymArHL-6((><7A3r)|SZ$c>?gSL8uUO)Kt7BuTwO>mnx(@w?M z)cC_TDR^tzX2{%K{tE4KE~N=tN;>3s3?p?-{#Rr2a`H@`J}|TC&CDdF{}8V^ZJ(kY zVmlj@H^H}4Y)4u D;}d*J literal 0 HcmV?d00001 diff --git a/lib/python/south/hacks/django_1_0.py b/lib/python/south/hacks/django_1_0.py new file mode 100644 index 0000000..db56110 --- /dev/null +++ b/lib/python/south/hacks/django_1_0.py @@ -0,0 +1,107 @@ +""" +Hacks for the Django 1.0/1.0.2 releases. +""" + +from django.conf import settings +from django.db.backends.creation import BaseDatabaseCreation +from django.db.models.loading import cache +from django.core import management +from django.core.management.commands.flush import Command as FlushCommand +from django.utils.datastructures import SortedDict + +class SkipFlushCommand(FlushCommand): + def handle_noargs(self, **options): + # no-op to avoid calling flush + return + +class Hacks: + + def set_installed_apps(self, apps): + """ + Sets Django's INSTALLED_APPS setting to be effectively the list passed in. + """ + + # Make sure it's a list. + apps = list(apps) + + # Make sure it contains strings + if apps: + assert isinstance(apps[0], basestring), "The argument to set_installed_apps must be a list of strings." + + # Monkeypatch in! + settings.INSTALLED_APPS, settings.OLD_INSTALLED_APPS = ( + apps, + settings.INSTALLED_APPS, + ) + self._redo_app_cache() + + + def reset_installed_apps(self): + """ + Undoes the effect of set_installed_apps. + """ + settings.INSTALLED_APPS = settings.OLD_INSTALLED_APPS + self._redo_app_cache() + + + def _redo_app_cache(self): + """ + Used to repopulate AppCache after fiddling with INSTALLED_APPS. + """ + cache.loaded = False + cache.handled = {} + cache.postponed = [] + cache.app_store = SortedDict() + cache.app_models = SortedDict() + cache.app_errors = {} + cache._populate() + + + def clear_app_cache(self): + """ + Clears the contents of AppCache to a blank state, so new models + from the ORM can be added. + """ + self.old_app_models, cache.app_models = cache.app_models, {} + + + def unclear_app_cache(self): + """ + Reversed the effects of clear_app_cache. + """ + cache.app_models = self.old_app_models + cache._get_models_cache = {} + + + def repopulate_app_cache(self): + """ + Rebuilds AppCache with the real model definitions. + """ + cache._populate() + + def store_app_cache_state(self): + self.stored_app_cache_state = dict(**cache.__dict__) + + def restore_app_cache_state(self): + cache.__dict__ = self.stored_app_cache_state + + def patch_flush_during_test_db_creation(self): + """ + Patches BaseDatabaseCreation.create_test_db to not flush database + """ + + def patch(f): + def wrapper(*args, **kwargs): + # hold onto the original and replace flush command with a no-op + original_flush_command = management._commands['flush'] + try: + management._commands['flush'] = SkipFlushCommand() + # run create_test_db + f(*args, **kwargs) + finally: + # unpatch flush back to the original + management._commands['flush'] = original_flush_command + return wrapper + + BaseDatabaseCreation.create_test_db = patch(BaseDatabaseCreation.create_test_db) + diff --git a/lib/python/south/hacks/django_1_0.pyc b/lib/python/south/hacks/django_1_0.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b9fdac7935449b47d2a54999d39aaeba27d1516e GIT binary patch literal 4798 zcmcIoTXWmS6+QqjB55gh8YivWPJ7C33Lb3$Qqs?|kPhTmRV^yq|pacq*$;3%?)Y zvj0Yti5#L0BnzboWFE?4C;`VUDOxgb%VFD(+fsC7-j&0yA9tkKl6g-Kdw$%NqA&A- z9A5L|Eh)BTz9WY_827|nlNomH%S(|LB3~Z9BH2K)9m%e(r>;x3E#uu+DBQ2m41S+Z zpW0|v)sZ_k(f&zV9#zrZ;hhn#;d@bS3X@u6hlBjT`0NIF#`XBLaV{^9te?4wnFqLU zKXvH@zk9VwU0#)bwS(1ZIz2XiIKXh8mg$k1o6`BYEzIpzb4+K=P2;L|Cfm=a&fUQJ z_-Vd)SS;=F>c+LXDbxd8_Ej_n=7tr7D#Ot6SofRqo)8y*|Afo7(cmt>e6Wj=?D7W3 z1#60#qfFI;YFNLFvDkeyqbJtXb~LFWZdH!Pxifa;mfxDJ`c08fM!Xi^xnr!^YUz$g z$F%Y&Q|l&olRHDK?qH*1Y*3h_tkU|(a>7ys33rJgwR4h`>D(lVqt%jRUS&%>ki>lq z8>zH+X`;nlY`%?4rRfHa4pg9AH2IfUI%je~`z37OBH-C+6Y1=9Bx`G@D_KW7TatCP z)01pVJADC?dGmm-qn1;egcohHhs!=e<3yeZ@*g1dwsO+vfIPqZvf2&)7J-3X9t|`oqEav-^)8eYBt4 zfBbkHt%4wORWvb?na#}9<=>d%I}N5Hw=PxG8FyojuC$! zqSVCmM|EA*F=j;P&uZmV4$Ekt+Et<7^o*<@;RS$ZP zMl|{b&our#P}rzBW7;kz4aF2ssYmT|6=osn*5;yQw=u8uChhIuqCx(MWtalt0&4(^ z?6l;_ciMtrM>ue@yV2h|y|c%qlm0maeu_OC0}}8+b8QyYVp*ilME4hqJtlxCojFrS zvpmZRfH-=VyW?{~wP{y!o|4bQw6G?ouN=9lsM5@2O3#Ewv14m#`Q&b=RSxL4{n1;z;SS%##v`vJe(`1TuTwXkvyz3@~Bwt~-y zV`R8BdNFQ{zlVHXd-P7L(t%hk@w;{a{E|kKA}ybSNZ^`ZN4APe^DOdqSvzr7S96Vl zPvVcGX<8}=Ww83De<-GUCH^^j$|Y5y#I-W>m0Hq~k*3tQVWfMQ@;S6~5&5^U2os(_ z)U)7kY3bVR{cD+(yt)m!&;#mm7S)^au%W0%_Pbd3IDQAc ztJUDgcmt9dsrtMQEcZ5bp!{nc7;C`lOIa0yD+K#&50(s(n%*VZgqf_YTr}g!Bg$3>ob^r$qEm^qBw_P+(W6)(e>;`SR~iRXENUUk zG)CDiJ>i9h8n=E3<@;X>3)qd>d;d^Bq0IdkBJ*>&>A7YBgeso7_l5e6G8|~KR7q9o zn40_CHuZ-qtF^s9UbL~ubBCtXr_Z!Y09SQ>l$U9d=tjxvNp(LW1Da_ub9v<*>&Is` zlCi1(g05xOmvAR&1vkQ1TfTClcJYu6Q>UL(2Ibu4iNd;JL2Ue)+tNT1Tpt{0l^FAV zn&Zv_#4l2b_zoH`8#A!lCvO^xMNI<0-P zi$u~e@%$H~1;D-ZV{BqX@Ezo0Qr|$&a@8;78`5QC!6E2XWKWvYyLWh7#ZtFf?|CDC h-Q@I-lhejTh-^$D+6yUrw?mgIf8WiqeYngH>kER(mxurW literal 0 HcmV?d00001 diff --git a/lib/python/south/introspection_plugins/annoying_autoonetoone.py b/lib/python/south/introspection_plugins/annoying_autoonetoone.py new file mode 100644 index 0000000..d61304f --- /dev/null +++ b/lib/python/south/introspection_plugins/annoying_autoonetoone.py @@ -0,0 +1,11 @@ +from django.conf import settings +from south.modelsinspector import add_introspection_rules + +if 'annoying' in settings.INSTALLED_APPS: + try: + from annoying.fields import AutoOneToOneField + except ImportError: + pass + else: + #django-annoying's AutoOneToOneField is essentially a OneToOneField. + add_introspection_rules([], ["^annoying\.fields\.AutoOneToOneField"]) diff --git a/lib/python/south/introspection_plugins/annoying_autoonetoone.pyc b/lib/python/south/introspection_plugins/annoying_autoonetoone.pyc new file mode 100644 index 0000000000000000000000000000000000000000..77c1f02c2daaf10ad2c2b84f02102b5b8369f23a GIT binary patch literal 551 zcmZWlT~ER=6uo!j!(b3dF#Z7_cyW95#TZG{goGdg{0I`4LCe_KT5P*Sp8d)G0KFX$ zV{F>f-rLiAPOtk_uU!VO!v*X|nf4o6S`aXr2q{2@FhxKcJCHh%xiEDpc7Y=#C0JKL zC9rK5FG`S>A*;Z&0{ARiP~ufs*FZf1BEBc^!d-}{QW+wIPX~~sSOvzNDoMuQG&)Ep zb7XCBV`35MUFM83A!9)-jFNi87bcb>Pb_Iie8EDS?)`S$UacJk_x%%t$pVjGxeTZ!n&8 z`~ADypgSCnO@mSg(Y@x#wWVzRUbd0S+$kkhheG%T9Z={$YOb_DSD|21_~Y1c?VJ3A zGkKB5bKkDb)~x8RFy;Sk&nC?yGUy-VgOGY61uwv(hxVUW^bu;~we_#9|0Xt_2D;eB HI+olYB)^M~ literal 0 HcmV?d00001 diff --git a/lib/python/south/introspection_plugins/django_audit_log.py b/lib/python/south/introspection_plugins/django_audit_log.py new file mode 100644 index 0000000..b874428 --- /dev/null +++ b/lib/python/south/introspection_plugins/django_audit_log.py @@ -0,0 +1,30 @@ +""" +South introspection rules for django-audit-log +""" + +from django.contrib.auth.models import User +from django.conf import settings +from south.modelsinspector import add_introspection_rules + +if "audit_log" in settings.INSTALLED_APPS: + try: + # Try and import the field so we can see if audit_log is available + from audit_log.models import fields + + # Make sure the `to` and `null` parameters will be ignored + rules = [( + (fields.LastUserField,), + [], + { + 'to': ['rel.to', {'default': User}], + 'null': ['null', {'default': True}], + }, + )] + + # Add the rules for the `LastUserField` + add_introspection_rules( + rules, + ['^audit_log\.models\.fields\.LastUserField'], + ) + except ImportError: + pass diff --git a/lib/python/south/introspection_plugins/django_audit_log.pyc b/lib/python/south/introspection_plugins/django_audit_log.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1669334a8b53da418f1abc10457fffbee786379a GIT binary patch literal 886 zcmb7C&5qMB5FRIK+NAB$0|*HTDaUeXaw`{vScyeisajR-B2cJ^(%Q*-*NKbmNbH%{ z;;;wahzEdiQo8C9TOQAk{rQ_2yT8Zb>+H*N2Zzze{T(j-3k3y8Q4*jI2m+M@sRP8? zE(jMY4^pqQJrF&pd`Nw?d%!%n?Zcf9>H+R)i{OJ8Kovk50AT^#hENbF0k{LS51ar) zWdQRMw*cleY<4fDWzs0~vk#_5dCm!(9mKClEr={#+yT5v-?U zn@x@)`ri?*Wn*q4UK=I#p6v{mb)*`>bW}(c(c8Q(~$k2>l9`uSb+kTuI7=-ozcsCSK&)Sb`rc>eckjUblEzM&&Nc%nQ`J1mm5=0`P5| zV=)dmD~y5}o}4LGZRXF=scqd3M|s_rPO4Q)(LQu}wZ1-IEH2(>=gZ~VOwe^?`tJdo zh_S6ZA=>75tr})}80yMpSzK0osmz5^Qccl>N1^cpLt-=4$yVhxm36Y_hUvsK-x-xN z!MBNB`+IYPv6gyr+Lpa&N=zf^>XAKi#XGjOZ?L%dE8e=O>j;JKObAZr32~i?7Z4Zc NQ}VKB_cLcq{sQFl)KUNd literal 0 HcmV?d00001 diff --git a/lib/python/south/introspection_plugins/django_objectpermissions.py b/lib/python/south/introspection_plugins/django_objectpermissions.py new file mode 100644 index 0000000..42b353b --- /dev/null +++ b/lib/python/south/introspection_plugins/django_objectpermissions.py @@ -0,0 +1,16 @@ +""" +South introspection rules for django-objectpermissions +""" + +from django.conf import settings +from south.modelsinspector import add_ignored_fields + +if 'objectpermissions' in settings.INSTALLED_APPS: + try: + from objectpermissions.models import UserPermissionRelation, GroupPermissionRelation + except ImportError: + pass + else: + add_ignored_fields(["^objectpermissions\.models\.UserPermissionRelation", + "^objectpermissions\.models\.GroupPermissionRelation"]) + diff --git a/lib/python/south/introspection_plugins/django_objectpermissions.pyc b/lib/python/south/introspection_plugins/django_objectpermissions.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7bea27de8e12ee4598ebb0dcd278f3cc30aff80b GIT binary patch literal 764 zcmah{QESvd5T4!Sa@R<;71YwFd(2BOpbz>W#ezi&r5uT(#Nu+f-AnhH?54Y$Vtw@| z`vY_~cPJ%_3;QKAUuNc;nFK#}j_8S5RC=dy73N?X-LP3FeJAgWX zCWInHI|M;tJ%Z~9%q`p<_7NkfBWT7@jG>u8F#+;X?L#Pb;5r6V0wLt)7Cwm(cm#Y5 z6@~8vh{Adk$detf;T1xh>&{(GrE*5wRxF*=YHGS#*y&1}DPJ#CrH^#EMyeI2k=EiD zHkLo}Bm^_KiO(gRld7^C&tvo~IA^j_+6c~8Qqm_6E0lsf$G+S!5 z%FqP&1i|{TZgoi2Pga|(pVvdA&o1)!C+Fv_5g8BQ+)1&}jXFkn7zfNDS< zz#^bH01?1d2)hXEE!_00iV##2@(30Yuy6V~grWtz4%n~282fn(mm-1$RAu#xF+$Fj37bmQXS2oq$=y)cn z%^1sT!OdGGSFu0E4Q?q{X^$W8r%)yBz{Pvv$-OSB@asCEsn7yH_*UF&GCkI7JYxat Qvn~t6h&=DV311NU4<$glCIA2c literal 0 HcmV?d00001 diff --git a/lib/python/south/introspection_plugins/django_taggit.py b/lib/python/south/introspection_plugins/django_taggit.py new file mode 100644 index 0000000..aded23f --- /dev/null +++ b/lib/python/south/introspection_plugins/django_taggit.py @@ -0,0 +1,14 @@ +""" +South introspection rules for django-taggit +""" + +from django.conf import settings +from south.modelsinspector import add_ignored_fields + +if 'taggit' in settings.INSTALLED_APPS: + try: + from taggit.managers import TaggableManager + except ImportError: + pass + else: + add_ignored_fields(["^taggit\.managers"]) diff --git a/lib/python/south/introspection_plugins/django_taggit.pyc b/lib/python/south/introspection_plugins/django_taggit.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7cb7d98acd645e00581e0da6f684d8c64deb8528 GIT binary patch literal 582 zcmY*V!A=4(5S{KW3u06-c<0Ck_UOeJN%UZXhOin98k>cdrO<7mEs37}WIsTsyF_D? zzV=P$b>2+)b<{kkFXK7vR}IfA+;)q=0H%lpSOAhh5x_J6;&})%gd&1zgggQsz^Vr8 z7)${lol;Q)8ADNrX&uOuYD2^uux^5R1wzQSfM*^7iy8@bETU{ZBK g_byQ2=Xrq*;N&m87u!OJgBEE8CnO{tazvuw2f|*F;s5{u literal 0 HcmV?d00001 diff --git a/lib/python/south/introspection_plugins/django_timezones.py b/lib/python/south/introspection_plugins/django_timezones.py new file mode 100644 index 0000000..d4b573d --- /dev/null +++ b/lib/python/south/introspection_plugins/django_timezones.py @@ -0,0 +1,21 @@ +from south.modelsinspector import add_introspection_rules +from django.conf import settings + +if "timezones" in settings.INSTALLED_APPS: + try: + from timezones.fields import TimeZoneField + except ImportError: + pass + else: + rules = [ + ( + (TimeZoneField, ), + [], + { + "blank": ["blank", {"default": True}], + "max_length": ["max_length", {"default": 100}], + }, + ), + ] + add_introspection_rules(rules, ["^timezones\.fields",]) + diff --git a/lib/python/south/introspection_plugins/django_timezones.pyc b/lib/python/south/introspection_plugins/django_timezones.pyc new file mode 100644 index 0000000000000000000000000000000000000000..73d0f383b9577f9322522d05e8a1ae35fec6ca72 GIT binary patch literal 658 zcmY*VU2oGc6g_UzbS)|rGz2fa_l4%Iydbnm3~ACdRc-lDKtyTnq+T7n*p9UE%8%It zKaU>(*Xg_+00l8~qs$-sE3rYuv3KohP*2Zwj0~1_jg%83PM-2daZQLKaaS zK}5KUu}xraadX^u#86$-39|WC@dvDjX&Ip+5q$_^_rd$2{M7-|q-lV~_z~%qe`$>wW&Q<)g7aKzZ;jiDwUF8;^t;P(;QHr*VH&@g)P@V?q;BOIJEsfy;DcWkx-yfs(dE2# z^O(T%%f<5a;^ORmema{i{3wtQlJEX7j}9-K*E?hVnY9#S0++TCt%3an2*LP1xQx>D zqZ8JpD_dx8bh?mUxYRe-f}7V$uF|k~H-1BRt@`x;$?jBBk&|>UJ%3O}Jz>;u$t(^3 aU9jU`QW&vO=Lw70F&nZtPACictN0&5cAN?T literal 0 HcmV?d00001 diff --git a/lib/python/south/introspection_plugins/geodjango.py b/lib/python/south/introspection_plugins/geodjango.py new file mode 100644 index 0000000..bece1c9 --- /dev/null +++ b/lib/python/south/introspection_plugins/geodjango.py @@ -0,0 +1,45 @@ +""" +GeoDjango introspection rules +""" + +import django +from django.conf import settings + +from south.modelsinspector import add_introspection_rules + +has_gis = "django.contrib.gis" in settings.INSTALLED_APPS + +if has_gis: + # Alright,import the field + from django.contrib.gis.db.models.fields import GeometryField + + # Make some introspection rules + if django.VERSION[0] == 1 and django.VERSION[1] >= 1: + # Django 1.1's gis module renamed these. + rules = [ + ( + (GeometryField, ), + [], + { + "srid": ["srid", {"default": 4326}], + "spatial_index": ["spatial_index", {"default": True}], + "dim": ["dim", {"default": 2}], + "geography": ["geography", {"default": False}], + }, + ), + ] + else: + rules = [ + ( + (GeometryField, ), + [], + { + "srid": ["_srid", {"default": 4326}], + "spatial_index": ["_index", {"default": True}], + "dim": ["_dim", {"default": 2}], + }, + ), + ] + + # Install them + add_introspection_rules(rules, ["^django\.contrib\.gis"]) \ No newline at end of file diff --git a/lib/python/south/introspection_plugins/geodjango.pyc b/lib/python/south/introspection_plugins/geodjango.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1cb2edb34ad4ede4159de687a189e86e4d526a41 GIT binary patch literal 1008 zcmZuv%Wl&^6g{>R=iP)-gjgUVA)yF~co#MZQK>D8R6>Xgi%?-XnTa#0ACYG)HEX^V zet-{vJCig~Mg6#*d+uw_89RSk^|!(I@f^FWO5YFk%1;)+fGJrE%tmZsN(*`%#5NKK zQwNszONdKI%9xhPFM~T+xwvsLt$tj#vW@D)-riVd#puR+vGL(SQ~?dgNXRFkbVK* z&vC#y56pS=z?|d%=j?lO(x3(QSdaK=!%9g)8-DLZ%ld*${UguLR$&@tU6HChlWRU# zB1^k@5p!7=f9V?aNPj4O9Fi*~(nxNRr-X!z1@|fgQz?%r#!Tb>JfqHH){n%lu0=7L zn{btHE(DL6AV9OADJ3#87fk%_kXIp(@kLm~O6e-O4wVRFn#lOAa&%NA!ltjPk%@Q~ z<>7j{QDyQ1-AB24wawSP11gYDXz^payX(8{U7P(KQN&CQ0+!8#z?5$@RfA#&N{b1( zP`Z`O3bpJf8RM}OsgW?tCx(vhHyrs_r0mTvqkWGSI2h@Z>wT3xm)36+RONtpb-u*LO$3;ZUJQ8@fT}swJ#5-b5f69A(6!JA0*KS%JyJNR) V*X}s4O0MP5ePZrz<={Pc{{f$A;-~-s literal 0 HcmV?d00001 diff --git a/lib/python/south/logger.py b/lib/python/south/logger.py new file mode 100644 index 0000000..26b74d1 --- /dev/null +++ b/lib/python/south/logger.py @@ -0,0 +1,38 @@ +import sys +import logging +from django.conf import settings + +# Create a dummy handler to use for now. +class NullHandler(logging.Handler): + def emit(self, record): + pass + +def get_logger(): + "Attach a file handler to the logger if there isn't one already." + debug_on = getattr(settings, "SOUTH_LOGGING_ON", False) + logging_file = getattr(settings, "SOUTH_LOGGING_FILE", False) + + if debug_on: + if logging_file: + if len(_logger.handlers) < 2: + _logger.addHandler(logging.FileHandler(logging_file)) + _logger.setLevel(logging.DEBUG) + else: + raise IOError, "SOUTH_LOGGING_ON is True. You also need a SOUTH_LOGGING_FILE setting." + + return _logger + +def close_logger(): + "Closes the logger handler for the file, so we can remove the file after a test." + for handler in _logger.handlers: + _logger.removeHandler(handler) + if isinstance(handler, logging.FileHandler): + handler.close() + +def init_logger(): + "Initialize the south logger" + logger = logging.getLogger("south") + logger.addHandler(NullHandler()) + return logger + +_logger = init_logger() diff --git a/lib/python/south/logger.pyc b/lib/python/south/logger.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d9458c31a1bab1217e3c638c88444dec8152bf8a GIT binary patch literal 1971 zcmb_dO>f&q5S=AuSyJuVF;D|R4_%;#^3X~^F9m`mZR4*PFrrIDoXt$tXB&c>W#%>!3&)WPe3nvZ6czE(M3L*UC5o11 zy2@D%@zH$>vU*@1A&N$Ql0#bwlt>3h2|R!B!tujUDsmp(`gF6lcy|^>?-`>TdatOu zBQw8K#Je9 z_|dsInd?|jv)t%;O#tmm?dC?;@ak*|tui{Z#aB+3h0$?dnK-@byIqbk9-Y5>8J&$z zPELm>(P-%QJ}@4io*iYt!5)3EHyUza{i<4-zJ6V4NZ|&*bt{_!+P<98;^uc#4#gsx7XoV9P-S( zHMtydc=Y1@L_Rqk9aU9X)f|UkYA#l@s4N^3Yyw7r_nvAGWSL~nRX0_E)fSkOx2N{J zwrZ=-)V|vHw!8#ZaB|JRLss5B0|gg~4uAc&Vc)_7j4#T0L`+=%}*)KNwovnSPaAr0BrLl^Z)<= literal 0 HcmV?d00001 diff --git a/lib/python/south/management/commands/__init__.py b/lib/python/south/management/commands/__init__.py new file mode 100644 index 0000000..da218eb --- /dev/null +++ b/lib/python/south/management/commands/__init__.py @@ -0,0 +1,40 @@ + +# Common framework for syncdb actions + +import copy + +from django.core import management +from django.conf import settings + +# Make sure the template loader cache is fixed _now_ (#448) +import django.template.loaders.app_directories + +from south.hacks import hacks +from south.management.commands.syncdb import Command as SyncCommand + +class MigrateAndSyncCommand(SyncCommand): + """Used for situations where "syncdb" is called by test frameworks.""" + + option_list = copy.deepcopy(SyncCommand.option_list) + + for opt in option_list: + if "--migrate" == opt.get_opt_string(): + opt.default = True + break + +def patch_for_test_db_setup(): + # Load the commands cache + management.get_commands() + # Repoint to the correct version of syncdb + if hasattr(settings, "SOUTH_TESTS_MIGRATE") and not settings.SOUTH_TESTS_MIGRATE: + # point at the core syncdb command when creating tests + # tests should always be up to date with the most recent model structure + management._commands['syncdb'] = 'django.core' + else: + management._commands['syncdb'] = MigrateAndSyncCommand() + # Avoid flushing data migrations. + # http://code.djangoproject.com/ticket/14661 introduced change that flushed custom + # sql during the test database creation (thus flushing the data migrations). + # we patch flush to be no-op during create_test_db, but still allow flushing + # after each test for non-transactional backends. + hacks.patch_flush_during_test_db_creation() diff --git a/lib/python/south/management/commands/__init__.pyc b/lib/python/south/management/commands/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d92659e488e7f57f9b3673f6d322ffb28f2ef1f9 GIT binary patch literal 1464 zcmcgsOK;mo5FS!b%XT9qhzs{bq%f+Br9%(B1VLU#nuAl9QV@hiv7opsi85c|4v`o+ zI{!F7p#5g)Sji7a2<~uZc6N8>`)1ksd^q|;{d1Af&C|j6F^2sJq49c@5k;-;Q|3|b zQ{l?yn4RczoZR_d5vx$?-8*>2jS(#&< z=KIjk({&xQc~a>4Rgv7xd{g^V+42IyAQdDC>67Wuwo9f*+df@;QsIYFGJSgf?+HZ% z$=3{MJCs5$eX38$?1@K>=;GZ8;e8X%C%)|b7~-`xI#`u;U{h{lPRqgu?>DA4!2^q| z^zuQF+8~Lu3~|e=fQ{u~RmZt`U)Gm)Y6Y6p)4Z)^vJZ;|38f0eDCMA1Dlc`Di76l` zrOO0*7kW}wS8jnergDaC@f9E}c3DYgDoZW1GzDz_3E#D01zlOL0lV99Q8&h&qRlF9 zGM*recEZ6qL)EpNEo)Fw7PEPZs%G4LFuFX=(&bDB(%N9omJM%aJFaI*%b=Ypl@=*0 zHLb2fspJlZJ%H$YegCe1=-u`9yjwaY;+aJJ4Z%cPPYCkZClmzEu0hrBqJ!H5Xc`lU z>PcTeEPf#d+3mvCg%VqirT6C(+5Q#ZgPi~Mdhw@PoXr<=b^h{2_+)Wr1wy@zi*-3o z%GzKrB;we0ogKm?med8$F9kXfH?fVG>rlEr97~w+eH-Hn52cy-SBNU+WTRGDV>e1S zLWz=oQF^J8+PLo8G2yiDLI?l6%cZPuQ^4hHs$)!9xxVC&{Nv8pANMEHN#PK}_1afJ zwZMFXUa@jm@NWpeiDZ*k87}fPD`Snmn#NV7bXuE)%Q{8<0(Tqh4sb0sbw7p{x&ZU7 wbJu4e%Ka&D!yHjz2SK4E-eP_P?T26~xIApH?<46lD`*|~V{hDX(mxvg50peMMITqm6SER&P^_isgO`1P?g1Km&6f8rbaU@X|(er zrduYHvadF8!7K3uyao>d-#I~$6We1cW(&W&s`;;9~eoT{Nx89}fgz^DR25!Aa*?r0% z(BuKu`=mQGgN6t6J<$!(*OP~=?&a{1t@;Uu{%_Z5e68ZLc&#gwmIbDH9uZ%Fx(4({~i3txb!$ag;8LD-)eH z>%NFDqPfm>VI9wX(0-M#s&b{Ptp2ntX6bxg>BQ~wi7(Q56|p*vln!W%D86o%dstqS zcT9E%ghtidK(jkO!Ds%8K_k!}=@us|@Ljf%qc|EN+|$yewzeFG zC}s~@_MNe1rEMFtL>p^+nB4KQ^AbIa)|s`O63>Lu)+;Sy;zgOpT3qa=M!3#p{7`ro zI$N2KarDcL0xHw5b@p7HzsS=f&7(|}Yr9(8r|Q{@qO7(FAHImP&9-?DLPjH`kG6U= zUzEm<^4nanoElhGn6R zN(*HdS|yQrp)*WXrO} ztf0Pa(^thKYFnSK`&5m|^E}`Rn-86cSIiAUTev&)rbFgC#ss#zTYU8aA-$bY@2lb% zBD*5;&n&X&(<`9ViA4Us@o5S42xxCay|-Ng7M1{TpQR+bTn@qTrb{=imS^{92{aSQ z?7+=Bv^;b(cXCfo?sGl9J)9hFE;}T91X+yv=K7XXf6&?r3QDtTdk zjPYfrIV;Ud$LXv_4sZlHa!qUlfnp#<-2^yrwa}I3I2NVSMKsO+CFpr%;|0g{^ocnQ z442HS1=u;8Hi@x_IM{7RKVLz>I!K zdA6ld#mP(5w021zj5&Ezd}O6#{_`zT=bB4o?(TQR^ZfkW6~`H@7;~)}OZEL4v!5d* z?9NS4bP}kG(gLOI7>BROm^A~Ci_Mm=PtC?x5}1ZzQ;kdn>BBV8fvU^3xVAX8Dqcjz z9IOB=AR23xMFvv=>if?XpuFLJDl@Yv*IBYDhpfR9b9U=oV?M(zNm}U`t)hOaD%9`F zHhM`kO1{b|=!(fk!6t+iGIJiKMKC;uDA#t7PIk;~M_f7AHiX$@Zcb9mnc%Wy%vEK~ zeF;SY;VG>Af?>@43rHl2(I>(t1*Pp=p$UxnQ6hnifs%^lhjE0MTEu+^Zju)eZ7|_CoeeFX zVa$ZcNRvh~B$-3zqg@iVOMeD=S0+?bye1AOL37`m;VH-rNoc@Mm!7XNnf?fc!< z@EvSR?87iAV>p1l0fSaiWwe+h=9=2rN7zZ0KxP@lcq#<0`fv;bepxtQJ^s&3aha9X znA@y)^j1_c8#6BC<<=0BsY4dW=a#S14lnH4En=X*jkUb*jYFSQK1rb45+D$iTU} 0) + + # See what filename is next in line. We assume they use numbers. + new_filename = migrations.next_filename(name) + + # Work out which apps to freeze + apps_to_freeze = self.calc_frozen_apps(migrations, freeze_list) + + # So, what's in this file, then? + file_contents = MIGRATION_TEMPLATE % { + "frozen_models": freezer.freeze_apps_to_string(apps_to_freeze), + "complete_apps": apps_to_freeze and "complete_apps = [%s]" % (", ".join(map(repr, apps_to_freeze))) or "" + } + + # - is a special name which means 'print to stdout' + if name == "-": + print file_contents + # Write the migration file if the name isn't - + else: + fp = open(os.path.join(migrations.migrations_dir(), new_filename), "w") + fp.write(file_contents) + fp.close() + print >>sys.stderr, "Created %s." % new_filename + + def calc_frozen_apps(self, migrations, freeze_list): + """ + Works out, from the current app, settings, and the command line options, + which apps should be frozen. + """ + apps_to_freeze = [] + for to_freeze in freeze_list: + if "." in to_freeze: + self.error("You cannot freeze %r; you must provide an app label, like 'auth' or 'books'." % to_freeze) + # Make sure it's a real app + if not models.get_app(to_freeze): + self.error("You cannot freeze %r; it's not an installed app." % to_freeze) + # OK, it's fine + apps_to_freeze.append(to_freeze) + if getattr(settings, 'SOUTH_AUTO_FREEZE_APP', True): + apps_to_freeze.append(migrations.app_label()) + return apps_to_freeze + + def error(self, message, code=1): + """ + Prints the error, and exits with the given code. + """ + print >>sys.stderr, message + sys.exit(code) + + +MIGRATION_TEMPLATE = """# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + "Write your forwards methods here." + # Note: Remember to use orm['appname.ModelName'] rather than "from appname.models..." + + def backwards(self, orm): + "Write your backwards methods here." + + models = %(frozen_models)s + + %(complete_apps)s + symmetrical = True +""" diff --git a/lib/python/south/management/commands/datamigration.pyc b/lib/python/south/management/commands/datamigration.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e22810b335d5786d0e8a23e9d0cc491fe8ed0913 GIT binary patch literal 4643 zcmcgwZEqa65uUv}ourdw$+9dvb?eej4V_cnsb~tPb=shgBPVWcSy;(&bFQ-3+vVw2 z+L!B+XIX)Sez3o{K!0L?LV`DXmd9L!BA&PUlG1$$OUHpqieg z;|7(QJkR6#HA?3wo2L#Ga*zJNvKdMnlr2zafkdZH=^|xI)LC-yoJ-Ke4^)ZV1D=Lla_DEOgs7kuVx9_ey#{Hr@LvfwrS&HZAl~-eh zc~NK(D=ZNE6*^E@VufCXt}rs-S4_jzVc;^wD_pbQMT%GHHQ)j(m-r$&Fa+Tmy#~}4 z$lq(O^Ul~ot(H54W#5Dm``0xG1D(gV25GE~l{;{nL7Je?1NL;FMcLRAE)-c`rvr10 z!3W|SW&2v0fsT@1qGJ^f22FFjt#-@eMG|Wj<}6Zqm}zyA*uK(X)c;Tojh2b-zEw6% zZ8At9OEPU#uPCP?OgE#07#kNuYxxR|ElM5OayZ_fjPNzZx);Kn`3UV@?AppgQy|(x zIXf!Jjn!eSik>Id)gGM6yy;$WHu20vBE9FWmfPWhLet#|dyxkZEbLOkQ4{fn zG6>lpzeh1FKC9ATo(_r5t8`YQ@*3mpyh1r{ljuk|=&VAoDpWosG%$Kxl{QAvtpb1G z78t6K>Cu^!)}&FTE8{_r%&YO>%v*zi$r;wElNCsxp`N4T8Vpy?XX$K~j_Pb3rq1VN zstB=hZ0dZT>>MQ(vh#G*ptE^aL$KUItNbU$!1)3lEs$L#yClP6XnpW6jv++Po%M4} zbLYYt-|@~G^yz2$BeECBF4NHp&*nW`WcE5kJVN(^EDbqquM%ePF}ZVzl{~vfN0+IF zK!pB!<>BuT-h0gzxO~U*O|%SO#q%$NKb{8T3S zK|jognJ$w^Mg6cuZ0Zs~%VRanV>BZ`+_cS4VepG$h=?#&4aRZhLdxMTF;k*#nD@U` z-^C%oEkvw==LzUI-r5cL2YgqQkq*XOZTW~VblEM;RG2`D>I?MqXM*2g-R9qTZkhRg%g)MQiJ+0Z6yHB@kQWTYET-*|Niz zIxbpi(%s@RIJJF@8PIlr#DWYgQMlb#K0U z-CM!$l?qDrdUd(Fir$LXsx5kR)t`7vUc-y{QWzxsc-==ae?#TS@a30G3!fZ(PUm=j zJtoKFI+gDW1L4`Iuw;G2Z-6Dp5CZhg=VKLN)UgWq3U9qr@&2?#&u}kHc&O|23Y7X6 z=3-WXty$)gk!?u#n!HK#XH*R}`g>NC$40?Xn+hn+l8IJbt1{1qs6fc6*fM4J+OGz5_aKKn*e zTo)#i;`%&Y2>3ym8twCI=Zl}&@J^9y|5q&H+Y<0iPbaPKjs<;2?o|{4NZqS@*Q6|g z`|6det@%~+cO)L~s@8igut1E|^{yJ)Uh9`EX(ZVIPbBA>niJ}V@QHA6JMOCSX!o1) z7e61%`Md4$_yftQ#7AL%SbSryd-5BNC=HEK6Yb_3b8p{m2oRBwbB1vema%E_L$(Q~ z$S^u)zMIeZC1j>4CyENMcE5;?>g!UsZ%%aIRXc@6PVMVVXI+h~yO56wSbV;LSm)bs zKjtsM9kezM6h1#;0wDmpxhakqYq>+T+ps>_XE%(F|G$k+G~eE-*mAa!0o`gk#vNrz zw~brtR`ZQ42x4}IIn8iv~shrC+nm+&vB-DZ+E+QaVVv_OaU*jPLJup28OCT6+ za(M*4c^O0Ogl$Qf7}9t_j1Sz9<*B1vN{<+`Edi);{JB;oC literal 0 HcmV?d00001 diff --git a/lib/python/south/management/commands/graphmigrations.py b/lib/python/south/management/commands/graphmigrations.py new file mode 100644 index 0000000..eb98487 --- /dev/null +++ b/lib/python/south/management/commands/graphmigrations.py @@ -0,0 +1,61 @@ +""" +Outputs a graphviz dot file of the dependencies. +""" + +from optparse import make_option +import re +import textwrap + +from django.core.management.base import BaseCommand +from django.core.management.color import no_style + +from south.migration import Migrations, all_migrations + +class Command(BaseCommand): + + help = "Outputs a GraphViz dot file of all migration dependencies to stdout." + + def handle(self, **options): + + # Resolve dependencies + Migrations.calculate_dependencies() + + colors = [ 'crimson', 'darkgreen', 'darkgoldenrod', 'navy', + 'brown', 'darkorange', 'aquamarine' , 'blueviolet' ] + color_index = 0 + wrapper = textwrap.TextWrapper(width=40) + + print "digraph G {" + + # Group each app in a subgraph + for migrations in all_migrations(): + print " subgraph %s {" % migrations.app_label() + print " node [color=%s];" % colors[color_index] + for migration in migrations: + # Munge the label - text wrap and change _ to spaces + label = "%s - %s" % ( + migration.app_label(), migration.name()) + label = re.sub(r"_+", " ", label) + label= "\\n".join(wrapper.wrap(label)) + print ' "%s.%s" [label="%s"];' % ( + migration.app_label(), migration.name(), label) + print " }" + color_index = (color_index + 1) % len(colors) + + # For every migration, print its links. + for migrations in all_migrations(): + for migration in migrations: + for other in migration.dependencies: + # Added weight tends to keep migrations from the same app + # in vertical alignment + attrs = "[weight=2.0]" + # But the more interesting edges are those between apps + if other.app_label() != migration.app_label(): + attrs = "[style=bold]" + print ' "%s.%s" -> "%s.%s" %s;' % ( + other.app_label(), other.name(), + migration.app_label(), migration.name(), + attrs + ) + + print "}"; diff --git a/lib/python/south/management/commands/graphmigrations.pyc b/lib/python/south/management/commands/graphmigrations.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c550a234adad8282beb196b37222139319adeb0 GIT binary patch literal 2158 zcmcgt+ioL85UrkxCywK6OxWDGh*6{ zw-X0!9#~%a20ns!eu@u(Q|+u1Zja!oD_zx9)u(Pf{%>oQ52CZrdvyEr@&98i^F4?p zYJ&=r9;JdZkJ=sy=6yA)0i`RHRj6H| zY?a!pkS>!7s1KiI`jO}*(bw%NUvN@iVH}S@Dlf)%G`1#;!}B5@4X%=JL#b`pPf``? zerN|Olxn1Msq$W;Orw(gj$d8C)~VHv8qj*PuY zRo!Fb3dm;(E@e|=AApV1G|KKo%MjfAW83KQl?;}1_9>S684^o06r565GhB~?p-v@|0U__m z_<5OTkgw3pC+pD=3Kf#8un{w8R_XZJ|`K0E4UG{X(+G!1SQc@wZj-=nSBGTCJsmPyt~zDCn!dg&8{a)X8|G+m-$#R&mj zIQi@U;0RWIp8;G>=u|!gP+*Xow2guT5G=P4)8z{~o&2;2;0F$1ax({c$si{K=er8f z9hw2s7R`X6d=Iya^unj2E#x+z=9r0{dpb~dvHd;T=?e8r4&;G2y?h(1CQjupz@i^c z>TjS>&yfnEUXf%*=a$PS;I$k|>e(hib3Si=J%xuHjc@HqU=tb-3jGw#ks`v$Uy zP8No_mMT2!=~Neo`=;~QFnDMjg!{(u#6<5|ZpF|s-{fZZ)^Km%H1^G2c$UUpl^%lK zgSGEYX9=5_D#+QjO3nxN@KNLa&RpPz@vw^wbj*%Zf92}nN%(5JZyvjAXLU|li{08S zkY1ej#%XL-bpLs@{L`@52{hPQi>+(c%=lI)IHF+S{yW3KK$9D9iqlA15E` zBzGIK^+B3+o4iIhc7QX3 zr)AK@`xu|A3{f?E^ViZeU281vHw_qY90T}F)dqIvLlE*SUR7*}Kx}v=zb4kjnppFE zu`4#kj_}1hq9k@8UlTPy@OH!sWE=R~^y{t)T&F0?<4i@7OF$H5T8>j@Sz(~kQR`i7 z?n3;}<7{z;`8!}3LEr_R>p54eD3ZDdY>XS_9mR!FE$peq&Di3+*f&As5TmLaJzc2A zU3ME?e2-c@3ihpkS91N)avvuiR9B+Lq7tt4T?;#$|6^WU&>zjq^TZ7ZCn@l1qAIGO IHE+xN10yu{NdN!< literal 0 HcmV?d00001 diff --git a/lib/python/south/management/commands/migrate.py b/lib/python/south/management/commands/migrate.py new file mode 100644 index 0000000..0ef494f --- /dev/null +++ b/lib/python/south/management/commands/migrate.py @@ -0,0 +1,260 @@ +""" +Migrate management command. +""" + +import os.path, re, sys +from optparse import make_option + +from django.core.management.base import BaseCommand +from django.conf import settings +from django.utils.importlib import import_module + +from south import migration +from south.migration import Migrations +from south.exceptions import NoMigrations +from south.db import DEFAULT_DB_ALIAS + +class Command(BaseCommand): + option_list = BaseCommand.option_list + ( + make_option('--all', action='store_true', dest='all_apps', default=False, + help='Run the specified migration for all apps.'), + make_option('--list', action='store_true', dest='show_list', default=False, + help='List migrations noting those that have been applied'), + make_option('--changes', action='store_true', dest='show_changes', default=False, + help='List changes for migrations'), + make_option('--skip', action='store_true', dest='skip', default=False, + help='Will skip over out-of-order missing migrations'), + make_option('--merge', action='store_true', dest='merge', default=False, + help='Will run out-of-order missing migrations as they are - no rollbacks.'), + make_option('--no-initial-data', action='store_true', dest='no_initial_data', default=False, + help='Skips loading initial data if specified.'), + make_option('--fake', action='store_true', dest='fake', default=False, + help="Pretends to do the migrations, but doesn't actually execute them."), + make_option('--db-dry-run', action='store_true', dest='db_dry_run', default=False, + help="Doesn't execute the SQL generated by the db methods, and doesn't store a record that the migration(s) occurred. Useful to test migrations before applying them."), + make_option('--delete-ghost-migrations', action='store_true', dest='delete_ghosts', default=False, + help="Tells South to delete any 'ghost' migrations (ones in the database but not on disk)."), + make_option('--ignore-ghost-migrations', action='store_true', dest='ignore_ghosts', default=False, + help="Tells South to ignore any 'ghost' migrations (ones in the database but not on disk) and continue to apply new migrations."), + make_option('--noinput', action='store_false', dest='interactive', default=True, + help='Tells Django to NOT prompt the user for input of any kind.'), + make_option('--database', action='store', dest='database', + default=DEFAULT_DB_ALIAS, help='Nominates a database to synchronize. ' + 'Defaults to the "default" database.'), + ) + if '--verbosity' not in [opt.get_opt_string() for opt in BaseCommand.option_list]: + option_list += ( + make_option('--verbosity', action='store', dest='verbosity', default='1', + type='choice', choices=['0', '1', '2'], + help='Verbosity level; 0=minimal output, 1=normal output, 2=all output'), + ) + help = "Runs migrations for all apps." + args = "[appname] [migrationname|zero] [--all] [--list] [--skip] [--merge] [--no-initial-data] [--fake] [--db-dry-run] [--database=dbalias]" + + def handle(self, app=None, target=None, skip=False, merge=False, backwards=False, fake=False, db_dry_run=False, show_list=False, show_changes=False, database=DEFAULT_DB_ALIAS, delete_ghosts=False, ignore_ghosts=False, **options): + + # NOTE: THIS IS DUPLICATED FROM django.core.management.commands.syncdb + # This code imports any module named 'management' in INSTALLED_APPS. + # The 'management' module is the preferred way of listening to post_syncdb + # signals, and since we're sending those out with create_table migrations, + # we need apps to behave correctly. + for app_name in settings.INSTALLED_APPS: + try: + import_module('.management', app_name) + except ImportError, exc: + msg = exc.args[0] + if not msg.startswith('No module named') or 'management' not in msg: + raise + # END DJANGO DUPE CODE + + # if all_apps flag is set, shift app over to target + if options.get('all_apps', False): + target = app + app = None + + # Migrate each app + if app: + try: + apps = [Migrations(app)] + except NoMigrations: + print "The app '%s' does not appear to use migrations." % app + print "./manage.py migrate " + self.args + return + else: + apps = list(migration.all_migrations()) + + # Do we need to show the list of migrations? + if show_list and apps: + list_migrations(apps, database, **options) + + if show_changes and apps: + show_migration_changes(apps) + + if not (show_list or show_changes): + + for app in apps: + result = migration.migrate_app( + app, + target_name = target, + fake = fake, + db_dry_run = db_dry_run, + verbosity = int(options.get('verbosity', 0)), + interactive = options.get('interactive', True), + load_initial_data = not options.get('no_initial_data', False), + merge = merge, + skip = skip, + database = database, + delete_ghosts = delete_ghosts, + ignore_ghosts = ignore_ghosts, + ) + if result is False: + sys.exit(1) # Migration failed, so the command fails. + + +def list_migrations(apps, database = DEFAULT_DB_ALIAS, **options): + """ + Prints a list of all available migrations, and which ones are currently applied. + Accepts a list of Migrations instances. + """ + from south.models import MigrationHistory + applied_migrations = MigrationHistory.objects.filter(app_name__in=[app.app_label() for app in apps]) + if database != DEFAULT_DB_ALIAS: + applied_migrations = applied_migrations.using(database) + applied_migration_names = ['%s.%s' % (mi.app_name,mi.migration) for mi in applied_migrations] + + print + for app in apps: + print " " + app.app_label() + # Get the migrations object + for migration in app: + if migration.app_label() + "." + migration.name() in applied_migration_names: + applied_migration = applied_migrations.get(app_name=migration.app_label(), migration=migration.name()) + print format_migration_list_item(migration.name(), applied=applied_migration.applied, **options) + else: + print format_migration_list_item(migration.name(), applied=False, **options) + print + +def show_migration_changes(apps): + """ + Prints a list of all available migrations, and which ones are currently applied. + Accepts a list of Migrations instances. + + Much simpler, less clear, and much less robust version: + grep "ing " migrations/*.py + """ + for app in apps: + print app.app_label() + # Get the migrations objects + migrations = [migration for migration in app] + # we use reduce to compare models in pairs, not to generate a value + reduce(diff_migrations, migrations) + +def format_migration_list_item(name, applied=True, **options): + if applied: + if int(options.get('verbosity')) >= 2: + return ' (*) %-80s (applied %s)' % (name, applied) + else: + return ' (*) %s' % name + else: + return ' ( ) %s' % name + +def diff_migrations(migration1, migration2): + + def model_name(models, model): + return models[model].get('Meta', {}).get('object_name', model) + + def field_name(models, model, field): + return '%s.%s' % (model_name(models, model), field) + + print " " + migration2.name() + + models1 = migration1.migration_class().models + models2 = migration2.migration_class().models + + # find new models + for model in models2.keys(): + if not model in models1.keys(): + print ' added model %s' % model_name(models2, model) + + # find removed models + for model in models1.keys(): + if not model in models2.keys(): + print ' removed model %s' % model_name(models1, model) + + # compare models + for model in models1: + if model in models2: + + # find added fields + for field in models2[model]: + if not field in models1[model]: + print ' added field %s' % field_name(models2, model, field) + + # find removed fields + for field in models1[model]: + if not field in models2[model]: + print ' removed field %s' % field_name(models1, model, field) + + # compare fields + for field in models1[model]: + if field in models2[model]: + + name = field_name(models1, model, field) + + # compare field attributes + field_value1 = models1[model][field] + field_value2 = models2[model][field] + + # if a field has become a class, or vice versa + if type(field_value1) != type(field_value2): + print ' type of %s changed from %s to %s' % ( + name, field_value1, field_value2) + + # if class + elif isinstance(field_value1, dict): + # print ' %s is a class' % name + pass + + # else regular field + else: + + type1, attr_list1, field_attrs1 = models1[model][field] + type2, attr_list2, field_attrs2 = models2[model][field] + + if type1 != type2: + print ' %s type changed from %s to %s' % ( + name, type1, type2) + + if attr_list1 != []: + print ' %s list %s is not []' % ( + name, attr_list1) + if attr_list2 != []: + print ' %s list %s is not []' % ( + name, attr_list2) + if attr_list1 != attr_list2: + print ' %s list changed from %s to %s' % ( + name, attr_list1, attr_list2) + + # find added field attributes + for attr in field_attrs2: + if not attr in field_attrs1: + print ' added %s attribute %s=%s' % ( + name, attr, field_attrs2[attr]) + + # find removed field attributes + for attr in field_attrs1: + if not attr in field_attrs2: + print ' removed attribute %s(%s=%s)' % ( + name, attr, field_attrs1[attr]) + + # compare field attributes + for attr in field_attrs1: + if attr in field_attrs2: + + value1 = field_attrs1[attr] + value2 = field_attrs2[attr] + if value1 != value2: + print ' %s attribute %s changed from %s to %s' % ( + name, attr, value1, value2) + + return migration2 diff --git a/lib/python/south/management/commands/migrate.pyc b/lib/python/south/management/commands/migrate.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0162af6868b8fdad79fce2d9d639c353e664ecc5 GIT binary patch literal 8865 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 literal 0 HcmV?d00001 diff --git a/lib/python/south/management/commands/migrationcheck.py b/lib/python/south/management/commands/migrationcheck.py new file mode 100644 index 0000000..74eff58 --- /dev/null +++ b/lib/python/south/management/commands/migrationcheck.py @@ -0,0 +1,67 @@ +from django.core.exceptions import ImproperlyConfigured +from django.core.management import call_command, CommandError +from django.core.management.base import BaseCommand +from django.conf import settings +from django.db.models import loading +from django.test import simple + +from south.migration import Migrations +from south.exceptions import NoMigrations +from south.hacks import hacks + +class Command(BaseCommand): + help = "Runs migrations for each app in turn, detecting missing depends_on values." + usage_str = "Usage: ./manage.py migrationcheck" + + def handle(self, check_app_name=None, **options): + runner = simple.DjangoTestSuiteRunner(verbosity=0) + err_msg = "Failed to migrate %s; see output for hints at missing dependencies:\n" + hacks.patch_flush_during_test_db_creation() + failures = 0 + if check_app_name is None: + app_names = settings.INSTALLED_APPS + else: + app_names = [check_app_name] + for app_name in app_names: + app_label = app_name.split(".")[-1] + if app_name == 'south': + continue + + try: + Migrations(app_name) + except (NoMigrations, ImproperlyConfigured): + continue + app = loading.get_app(app_label) + + verbosity = int(options.get('verbosity', 1)) + if verbosity >= 1: + self.stderr.write("processing %s\n" % app_name) + + old_config = runner.setup_databases() + try: + call_command('migrate', app_label, noinput=True, verbosity=verbosity) + for model in loading.get_models(app): + dummy = model._default_manager.exists() + except (KeyboardInterrupt, SystemExit): + raise + except Exception, e: + failures += 1 + if verbosity >= 1: + self.stderr.write(err_msg % app_name) + self.stderr.write("%s\n" % e) + finally: + runner.teardown_databases(old_config) + if failures > 0: + raise CommandError("Missing depends_on found in %s app(s)." % failures) + self.stderr.write("No missing depends_on found.\n") +# +#for each app: +# start with blank db. +# syncdb only south (and contrib?) +# +# migrate a single app all the way up. any errors is missing depends_on. +# for all models of that app, try the default manager: +# from django.db.models import loading +# for m in loading.get_models(loading.get_app('a')): +# m._default_manager.exists() +# Any error is also a missing depends on. diff --git a/lib/python/south/management/commands/migrationcheck.pyc b/lib/python/south/management/commands/migrationcheck.pyc new file mode 100644 index 0000000000000000000000000000000000000000..271ed9011c01336c6a8bb06414dd81056158b363 GIT binary patch literal 2704 zcmcgu-ESL35T8AN#<7z$ZPO1x973&(09!m&DhTx>sVHru=$w*bRGrRuYx|u0>h4|} zgDs?{{|fIs&=>wBo{)G4X676xZJ)vR?(NLZ&CdMhV_o{I)wtpQu;;_#Q^Myh9Qs$B z0{jVx0Ac}s0fJ}?A`9XY^h*$zpVz?KW}py(n`1G>*L_V8-2Iy`xmB z3MH#3cE{6lu~RVX$kfu9Fd6BBQ$tRadI53@MwLXuIE!RKH;{f5j+AG@G+DevChVqf zb1TRld;Xy|q;zraGD2kg9UR)mNdg51K2#^OsX)iLb5{qk)ZX_&tg`E?WZrqyauxxOllC6;TT`5z?FHOSV0vIEl@wfvpq-v zrfXoBk3!-gxe8^o0;T~&3l1BgegPo_NeXykR$cs$L3$`&3_x-$5O{_8R8FF2WA*T&h8;Lo*GL0KJXVJui%8V4HN2Nz%Ti zzqGZKcAA?kH=MHLFfrQp%v<>>6F-#tX2Ve3cen*$+Mr^5bsQ2?)7jVxd|Bl6HQnIy zDoR4~I!VI>57H$R#eIDppO4lFZGQD_|MBMud!g998wh8-SBR`NojiH3?9dtRQofoHq{wnXg^vTVq{$|V*P0I7`AKt|eZAn68j=;aaexKAin zEssKNv^j_S59D-^dMdb|7(6Y{3@hnPwUP18QD`_>c8+|RQQO4g!Sf`_r!SH-W>Bwb zlYMN9meG*$;6_9ax``JTAx>G3W-}Cn&OqSVZmdVxAOMD#hj>e9KKkTk&08nbax3x% zGGf?p%b}}+?N6fsZxy<37(d2?S3w@fQ%+LZMq%Jde}}8?V=a~L4iw%*Y0~W>;<{_{ zBN?QhN8zALBb%Br?r~Bt8gDEU(_LI(y1VS}>^U}N_H&FXBT3QF-_W6AQ&dF*M^iMd z710vsaaFZSVhvX{v2I;P`jXWYB`Aq2VoO{?epRgFJ56yxd}wXr^Ssz?b2bxXE(7ek z?7U0y&m)N>W#(8$8RM1HmEmfm9Ezgz1x}|J{MSG@l$&(jYBIWt?o_SaHvK^`TQ~(= zjvYT$vLjEjO>?!pvtMK~0nh30Q1W2|d{fD(P=_kTp$fzHenu!67KKZ3SsWh>Iz{Oe z6?#UTioa+cT!qxtI?G^ld2_XKW}J0^g2TraJN&PLyA0#;|H04I^Hq@-UvoRA9N56z L+9 0) + + # What actions do we need to do? + if auto: + # Get the old migration + try: + last_migration = migrations[-2 if update else -1] + except IndexError: + self.error("You cannot use --auto on an app with no migrations. Try --initial.") + # Make sure it has stored models + if migrations.app_label() not in getattr(last_migration.migration_class(), "complete_apps", []): + self.error("You cannot use automatic detection, since the previous migration does not have this whole app frozen.\nEither make migrations using '--freeze %s' or set 'SOUTH_AUTO_FREEZE_APP = True' in your settings.py." % migrations.app_label()) + # Alright, construct two model dicts to run the differ on. + old_defs = dict( + (k, v) for k, v in last_migration.migration_class().models.items() + if k.split(".")[0] == migrations.app_label() + ) + new_defs = dict( + (k, v) for k, v in freezer.freeze_apps([migrations.app_label()]).items() + if k.split(".")[0] == migrations.app_label() + ) + change_source = changes.AutoChanges( + migrations = migrations, + old_defs = old_defs, + old_orm = last_migration.orm(), + new_defs = new_defs, + ) + + elif initial: + # Do an initial migration + change_source = changes.InitialChanges(migrations) + + else: + # Read the commands manually off of the arguments + if (added_model_list or added_field_list or added_index_list): + change_source = changes.ManualChanges( + migrations, + added_model_list, + added_field_list, + added_index_list, + ) + elif empty: + change_source = None + else: + print >>sys.stderr, "You have not passed any of --initial, --auto, --empty, --add-model, --add-field or --add-index." + sys.exit(1) + + # Validate this so we can access the last migration without worrying + if update and not migrations: + self.error("You cannot use --update on an app with no migrations.") + + # if not name, there's an error + if not name: + if change_source: + name = change_source.suggest_name() + if update: + name = re.sub(r'^\d{4}_', '', migrations[-1].name()) + if not name: + self.error("You must provide a name for this migration\n" + self.usage_str) + + # Get the actions, and then insert them into the actions lists + forwards_actions = [] + backwards_actions = [] + if change_source: + for action_name, params in change_source.get_changes(): + # Run the correct Action class + try: + action_class = getattr(actions, action_name) + except AttributeError: + raise ValueError("Invalid action name from source: %s" % action_name) + else: + action = action_class(**params) + action.add_forwards(forwards_actions) + action.add_backwards(backwards_actions) + print >>sys.stderr, action.console_line() + + # Nowt happen? That's not good for --auto. + if auto and not forwards_actions: + self.error("Nothing seems to have changed.") + + # Work out which apps to freeze + apps_to_freeze = self.calc_frozen_apps(migrations, freeze_list) + + # So, what's in this file, then? + file_contents = MIGRATION_TEMPLATE % { + "forwards": "\n".join(forwards_actions or [" pass"]), + "backwards": "\n".join(backwards_actions or [" pass"]), + "frozen_models": freezer.freeze_apps_to_string(apps_to_freeze), + "complete_apps": apps_to_freeze and "complete_apps = [%s]" % (", ".join(map(repr, apps_to_freeze))) or "" + } + + # Deal with update mode as late as possible, avoid a rollback as long + # as something else can go wrong. + if update: + last_migration = migrations[-1] + if MigrationHistory.objects.filter(applied__isnull=False, app_name=app, migration=last_migration.name()): + print >>sys.stderr, "Migration to be updated, %s, is already applied, rolling it back now..." % last_migration.name() + migrate_app(migrations, 'current-1', verbosity=verbosity) + for ext in ('py', 'pyc'): + old_filename = "%s.%s" % (os.path.join(migrations.migrations_dir(), last_migration.filename), ext) + if os.path.isfile(old_filename): + os.unlink(old_filename) + migrations.remove(last_migration) + + # See what filename is next in line. We assume they use numbers. + new_filename = migrations.next_filename(name) + + # - is a special name which means 'print to stdout' + if name == "-": + print file_contents + # Write the migration file if the name isn't - + else: + fp = open(os.path.join(migrations.migrations_dir(), new_filename), "w") + fp.write(file_contents) + fp.close() + verb = 'Updated' if update else 'Created' + if empty: + print >>sys.stderr, "%s %s. You must now edit this migration and add the code for each direction." % (verb, new_filename) + else: + print >>sys.stderr, "%s %s. You can now apply this migration with: ./manage.py migrate %s" % (verb, new_filename, app) + + +MIGRATION_TEMPLATE = """# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): +%(forwards)s + + def backwards(self, orm): +%(backwards)s + + models = %(frozen_models)s + + %(complete_apps)s""" diff --git a/lib/python/south/management/commands/schemamigration.pyc b/lib/python/south/management/commands/schemamigration.pyc new file mode 100644 index 0000000000000000000000000000000000000000..299e3a88eb4383a9923e2086e334e1a201b8633c GIT binary patch literal 8190 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 literal 0 HcmV?d00001 diff --git a/lib/python/south/management/commands/syncdb.py b/lib/python/south/management/commands/syncdb.py new file mode 100644 index 0000000..fe5af34 --- /dev/null +++ b/lib/python/south/management/commands/syncdb.py @@ -0,0 +1,111 @@ +""" +Overridden syncdb command +""" + +import sys +from optparse import make_option + +from django.core.management.base import NoArgsCommand, BaseCommand +from django.core.management.color import no_style +from django.utils.datastructures import SortedDict +from django.core.management.commands import syncdb +from django.conf import settings +from django.db import models +from django.db.models.loading import cache +from django.core import management + +from south.db import dbs +from south import migration +from south.exceptions import NoMigrations + +def get_app_label(app): + return '.'.join( app.__name__.split('.')[0:-1] ) + +class Command(NoArgsCommand): + option_list = syncdb.Command.option_list + ( + make_option('--migrate', action='store_true', dest='migrate', default=False, + help='Tells South to also perform migrations after the sync. Default for during testing, and other internal calls.'), + make_option('--all', action='store_true', dest='migrate_all', default=False, + help='Makes syncdb work on all apps, even migrated ones. Be careful!'), + ) + if '--verbosity' not in [opt.get_opt_string() for opt in syncdb.Command.option_list]: + option_list += ( + make_option('--verbosity', action='store', dest='verbosity', default='1', + type='choice', choices=['0', '1', '2'], + help='Verbosity level; 0=minimal output, 1=normal output, 2=all output'), + ) + help = "Create the database tables for all apps in INSTALLED_APPS whose tables haven't already been created, except those which use migrations." + + def handle_noargs(self, migrate_all=False, **options): + + # Import the 'management' module within each installed app, to register + # dispatcher events. + # This is copied from Django, to fix bug #511. + try: + from django.utils.importlib import import_module + except ImportError: + pass # TODO: Remove, only for Django1.0 + else: + for app_name in settings.INSTALLED_APPS: + try: + import_module('.management', app_name) + except ImportError, exc: + msg = exc.args[0] + if not msg.startswith('No module named') or 'management' not in msg: + raise + + # Work out what uses migrations and so doesn't need syncing + apps_needing_sync = [] + apps_migrated = [] + for app in models.get_apps(): + app_label = get_app_label(app) + if migrate_all: + apps_needing_sync.append(app_label) + else: + try: + migrations = migration.Migrations(app_label) + except NoMigrations: + # It needs syncing + apps_needing_sync.append(app_label) + else: + # This is a migrated app, leave it + apps_migrated.append(app_label) + verbosity = int(options.get('verbosity', 0)) + + # Run syncdb on only the ones needed + if verbosity: + print "Syncing..." + + old_installed, settings.INSTALLED_APPS = settings.INSTALLED_APPS, apps_needing_sync + old_app_store, cache.app_store = cache.app_store, SortedDict([ + (k, v) for (k, v) in cache.app_store.items() + if get_app_label(k) in apps_needing_sync + ]) + + # This will allow the setting of the MySQL storage engine, for example. + for db in dbs.values(): + db.connection_init() + + # OK, run the actual syncdb + syncdb.Command().execute(**options) + + settings.INSTALLED_APPS = old_installed + cache.app_store = old_app_store + + # Migrate if needed + if options.get('migrate', True): + if verbosity: + print "Migrating..." + management.call_command('migrate', **options) + + # Be obvious about what we did + if verbosity: + print "\nSynced:\n > %s" % "\n > ".join(apps_needing_sync) + + if options.get('migrate', True): + if verbosity: + print "\nMigrated:\n - %s" % "\n - ".join(apps_migrated) + else: + if verbosity: + print "\nNot synced (use migrations):\n - %s" % "\n - ".join(apps_migrated) + print "(use ./manage.py migrate to migrate these)" diff --git a/lib/python/south/management/commands/syncdb.pyc b/lib/python/south/management/commands/syncdb.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ce38f0b6f0a90918cb1568f697f84c7be31a8b26 GIT binary patch literal 4063 zcmcInTXP#p6+Sb%%d#!`7AH=^Y*AFSODyl^Who%3P2vT%*qGEP;2;vF#xreeLyIylkwZEbZD% zTt;({J~J@KU|#%66DY-#36HI0R#G(qWSxf0=i6evThf@ThIwMq)5qVz;w zaU@wN;$mFEiog$s*M#ES{)tFLS_ox(heri}1A`=TW1|y;w(Fh%zhi`!?IwkrDy?&Ru4-~KKMQBJP*X7L>PmQuDRg8k?rG6qjPw~;^ zIF7Cz(;tt!pptIn0eXACl_HmWM>&j`l5Da5Y^c*@LBFM^918rK+_%y4YejRBS|(Fvq4x<}&(?8N=J>J9oUzD{vpjJwT~Rvzu@bqXhC+v6&)GM!9Pl~eBu5B1 zusw&ueFx6qWMIu5IF-m#KXw8iyYwVG)$T6(CR(*q3yVdfd4S1v;QJj+j=EX9Fh z2mT=MJK_xzK%oydNQLY8&Bsl@>?jlzlWinDzJY2oE0e z>CLqKGlV?Ng|CE$G4z#xggoTnFNWF-)9zTEHcg}~|74Ogo z6!#SPAupUdM*_$MT6PGWE&?%D*o0S{52Ik{OwhYBW-s87;{3`^^lLowFQ7~}ieP|S zBo@t4T@KR@m6c7WP0H_)Wm)Mdy}URfr6X+r^(&f}Y1*L~T)9NEGQG92=p%&?<<_5TJjwS0Z1G`-_f6l&b}iEc!}o_VK=XF@XhOVaZa$0 z3Az2(5zQL}yUrRcW!|KjMfpEy1`hN&ddWKbD^1tAE{s|QX}&~<>0KOdS!+~^VCe@}+~u-_^n`EX_#S(vitZ5H5l=;AJ^4H|74F>RUy z`woq^cx;{h%{o*!Xtd3v(M>+}w;<0J=b0^}6zTxMTdYog$7WrccGxU**`X1tFyMs* zAPM!D;U_jyVc4S49X>41wWT%xzOYN}iTq1gwQ2!Gmi9yvdfk}D*4Wt%&OThH!Z&F(t#hdg_bDV@SZ z=ycx?yN@AGFZcv}dahU0L)5-SV&fcKUSGt@ZOqCgrMH;${ zw_a7Cm8H^U@ZjsXPNYizW3|gtbjsy2uIrM^0P1{<3qm4Ywu2V^#*Zhc8f>*7OH-li znTNEhTrL&vIy3Uo#DxeZ%G9rGbmj z$j4(3(FaG;)c=V*QS5tMa=cWC5WeE^4MlsPrWWQ2UFN$~RNq%LK4u<;S!%D-3$r+c zT2kS#BGgsoRJU&~XD`7*x`nBa^d(C-AL^zxM4gU>muBn__pqsag@!66Yr|T$%68i- zSsiN?{oB?`scKcNb*qK8ZI{qnwmYScwGDdRUbh6O>Pw{ADAMrjRvb!#27n>P2i z?W(nAb=ikpwzdY!D?-`xu34K|jH$dhlFG%1aB=kOETygS;f=1X_H$EO9%)N*X0F;T7<Z03ldWL8%xhgO$c^(~`tdd=XUY1~Wg)2f&?! zU}3G5yWF$y&hMRr&;8+Vm_~i-u==7E35qaeXB>9ukWigsmaBzr6z?lNSI%HtdtBYtrb#5XU?n-RDNMw@y%cAPg%8qZ>3qiX3x^RUfOqbeqWHm?#>Ml7%54=- zS0pvY_SI;6TCC;K8Z03p~5O2t4JtTb_(mL!hi3s9*W%=|1L0Cx^* z7gn%xmpkA2{NA(oxj#Hpk9R5jERX(IWcGqwMxTHJpcD`b5S&XWB$OV+9_1cr0V^No z0x}=o0p0)}Vjr>|OoKj6Sbot81VuPxr!89DJnN?0JEXlj!8BKM(R< zKtr>`&3LU`{>``eG7rg2&{tRZ=a?I5mwP?^sgqUfQVz fctnj(h+kXyaom&q4;f@l&pQ<_`EDoqNRH$;7bC_m literal 0 HcmV?d00001 diff --git a/lib/python/south/migration/__init__.py b/lib/python/south/migration/__init__.py new file mode 100644 index 0000000..45f3969 --- /dev/null +++ b/lib/python/south/migration/__init__.py @@ -0,0 +1,228 @@ +""" +Main migration logic. +""" + +import sys + +from django.core.exceptions import ImproperlyConfigured + +import south.db +from south import exceptions +from south.models import MigrationHistory +from south.db import db, DEFAULT_DB_ALIAS +from south.migration.migrators import (Backwards, Forwards, + DryRunMigrator, FakeMigrator, + LoadInitialDataMigrator) +from south.migration.base import Migration, Migrations +from south.migration.utils import SortedSet +from south.migration.base import all_migrations +from south.signals import pre_migrate, post_migrate + + +def to_apply(forwards, done): + return [m for m in forwards if m not in done] + +def to_unapply(backwards, done): + return [m for m in backwards if m in done] + +def problems(pending, done): + last = None + if not pending: + raise StopIteration() + for migration in pending: + if migration in done: + last = migration + continue + if last and migration not in done: + yield last, migration + +def forwards_problems(pending, done, verbosity): + """ + Takes the list of linearised pending migrations, and the set of done ones, + and returns the list of problems, if any. + """ + return inner_problem_check(problems(reversed(pending), done), done, verbosity) + +def backwards_problems(pending, done, verbosity): + return inner_problem_check(problems(pending, done), done, verbosity) + +def inner_problem_check(problems, done, verbosity): + "Takes a set of possible problems and gets the actual issues out of it." + result = [] + for last, migration in problems: + # 'Last' is the last applied migration. Step back from it until we + # either find nothing wrong, or we find something. + to_check = list(last.dependencies) + while to_check: + checking = to_check.pop() + if checking not in done: + # That's bad. Error. + if verbosity: + print (" ! Migration %s should not have been applied " + "before %s but was." % (last, checking)) + result.append((last, checking)) + else: + to_check.extend(checking.dependencies) + return result + +def check_migration_histories(histories, delete_ghosts=False, ignore_ghosts=False): + "Checks that there's no 'ghost' migrations in the database." + exists = SortedSet() + ghosts = [] + for h in histories: + try: + m = h.get_migration() + m.migration() + except exceptions.UnknownMigration: + ghosts.append(h) + except ImproperlyConfigured: + pass # Ignore missing applications + else: + exists.add(m) + if ghosts: + # They may want us to delete ghosts. + if delete_ghosts: + for h in ghosts: + h.delete() + elif not ignore_ghosts: + raise exceptions.GhostMigrations(ghosts) + return exists + +def get_dependencies(target, migrations): + forwards = list + backwards = list + if target is None: + backwards = migrations[0].backwards_plan + else: + forwards = target.forwards_plan + # When migrating backwards we want to remove up to and + # including the next migration up in this app (not the next + # one, that includes other apps) + migration_before_here = target.next() + if migration_before_here: + backwards = migration_before_here.backwards_plan + return forwards, backwards + +def get_direction(target, applied, migrations, verbosity, interactive): + # Get the forwards and reverse dependencies for this target + forwards, backwards = get_dependencies(target, migrations) + # Is the whole forward branch applied? + problems = None + forwards = forwards() + workplan = to_apply(forwards, applied) + if not workplan: + # If they're all applied, we only know it's not backwards + direction = None + else: + # If the remaining migrations are strictly a right segment of + # the forwards trace, we just need to go forwards to our + # target (and check for badness) + problems = forwards_problems(forwards, applied, verbosity) + direction = Forwards(verbosity=verbosity, interactive=interactive) + if not problems: + # What about the whole backward trace then? + backwards = backwards() + missing_backwards = to_apply(backwards, applied) + if missing_backwards != backwards: + # If what's missing is a strict left segment of backwards (i.e. + # all the higher migrations) then we need to go backwards + workplan = to_unapply(backwards, applied) + problems = backwards_problems(backwards, applied, verbosity) + direction = Backwards(verbosity=verbosity, interactive=interactive) + return direction, problems, workplan + +def get_migrator(direction, db_dry_run, fake, load_initial_data): + if not direction: + return direction + if db_dry_run: + direction = DryRunMigrator(migrator=direction, ignore_fail=False) + elif fake: + direction = FakeMigrator(migrator=direction) + elif load_initial_data: + direction = LoadInitialDataMigrator(migrator=direction) + return direction + +def get_unapplied_migrations(migrations, applied_migrations): + applied_migration_names = ['%s.%s' % (mi.app_name,mi.migration) for mi in applied_migrations] + + for migration in migrations: + is_applied = '%s.%s' % (migration.app_label(), migration.name()) in applied_migration_names + if not is_applied: + yield migration + +def migrate_app(migrations, target_name=None, merge=False, fake=False, db_dry_run=False, yes=False, verbosity=0, load_initial_data=False, skip=False, database=DEFAULT_DB_ALIAS, delete_ghosts=False, ignore_ghosts=False, interactive=False): + app_label = migrations.app_label() + + verbosity = int(verbosity) + # Fire off the pre-migrate signal + pre_migrate.send(None, app=app_label) + + # If there aren't any, quit quizically + if not migrations: + print "? You have no migrations for the '%s' app. You might want some." % app_label + return + + # Load the entire dependency graph + Migrations.calculate_dependencies() + + # Check there's no strange ones in the database + applied_all = MigrationHistory.objects.filter(applied__isnull=False).order_by('applied').using(database) + applied = applied_all.filter(app_name=app_label).using(database) + south.db.db = south.db.dbs[database] + Migrations.invalidate_all_modules() + + south.db.db.debug = (verbosity > 1) + + if target_name == 'current-1': + if applied.count() > 1: + previous_migration = applied[applied.count() - 2] + if verbosity: + print 'previous_migration: %s (applied: %s)' % (previous_migration.migration, previous_migration.applied) + target_name = previous_migration.migration + else: + if verbosity: + print 'previous_migration: zero' + target_name = 'zero' + elif target_name == 'current+1': + try: + first_unapplied_migration = get_unapplied_migrations(migrations, applied).next() + target_name = first_unapplied_migration.name() + except StopIteration: + target_name = None + + applied_all = check_migration_histories(applied_all, delete_ghosts, ignore_ghosts) + + # Guess the target_name + target = migrations.guess_migration(target_name) + if verbosity: + if target_name not in ('zero', None) and target.name() != target_name: + print " - Soft matched migration %s to %s." % (target_name, + target.name()) + print "Running migrations for %s:" % app_label + + # Get the forwards and reverse dependencies for this target + direction, problems, workplan = get_direction(target, applied_all, migrations, + verbosity, interactive) + if problems and not (merge or skip): + raise exceptions.InconsistentMigrationHistory(problems) + + # Perform the migration + migrator = get_migrator(direction, db_dry_run, fake, load_initial_data) + if migrator: + migrator.print_title(target) + success = migrator.migrate_many(target, workplan, database) + # Finally, fire off the post-migrate signal + if success: + post_migrate.send(None, app=app_label) + else: + if verbosity: + # Say there's nothing. + print '- Nothing to migrate.' + # If we have initial data enabled, and we're at the most recent + # migration, do initial data. + # Note: We use a fake Forwards() migrator here. It's never used really. + if load_initial_data: + migrator = LoadInitialDataMigrator(migrator=Forwards(verbosity=verbosity)) + migrator.load_initial_data(target, db=database) + # Send signal. + post_migrate.send(None, app=app_label) diff --git a/lib/python/south/migration/__init__.pyc b/lib/python/south/migration/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d54b0555ae488a9b26644685e0a61c6b5f7bcd74 GIT binary patch literal 7424 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 P>B8*!$=S(^c$@eS9Zzam literal 0 HcmV?d00001 diff --git a/lib/python/south/migration/base.py b/lib/python/south/migration/base.py new file mode 100644 index 0000000..feb0321 --- /dev/null +++ b/lib/python/south/migration/base.py @@ -0,0 +1,439 @@ +from collections import deque +import datetime +import os +import re +import sys + +from django.core.exceptions import ImproperlyConfigured +from django.db import models +from django.conf import settings +from django.utils import importlib + +from south import exceptions +from south.migration.utils import depends, dfs, flatten, get_app_label +from south.orm import FakeORM +from south.utils import memoize, ask_for_it_by_name, datetime_utils +from south.migration.utils import app_label_to_app_module + + +def all_migrations(applications=None): + """ + Returns all Migrations for all `applications` that are migrated. + """ + if applications is None: + applications = models.get_apps() + for model_module in applications: + # The app they've passed is the models module - go up one level + app_path = ".".join(model_module.__name__.split(".")[:-1]) + app = ask_for_it_by_name(app_path) + try: + yield Migrations(app) + except exceptions.NoMigrations: + pass + + +def application_to_app_label(application): + "Works out the app label from either the app label, the app name, or the module" + if isinstance(application, basestring): + app_label = application.split('.')[-1] + else: + app_label = application.__name__.split('.')[-1] + return app_label + + +class MigrationsMetaclass(type): + + """ + Metaclass which ensures there is only one instance of a Migrations for + any given app. + """ + + def __init__(self, name, bases, dict): + super(MigrationsMetaclass, self).__init__(name, bases, dict) + self.instances = {} + + def __call__(self, application, **kwds): + + app_label = application_to_app_label(application) + + # If we don't already have an instance, make one + if app_label not in self.instances: + self.instances[app_label] = super(MigrationsMetaclass, self).__call__(app_label_to_app_module(app_label), **kwds) + + return self.instances[app_label] + + def _clear_cache(self): + "Clears the cache of Migration objects." + self.instances = {} + + +class Migrations(list): + """ + Holds a list of Migration objects for a particular app. + """ + + __metaclass__ = MigrationsMetaclass + + if getattr(settings, "SOUTH_USE_PYC", False): + MIGRATION_FILENAME = re.compile(r'(?!__init__)' # Don't match __init__.py + r'[0-9a-zA-Z_]*' # Don't match dotfiles, or names with dots/invalid chars in them + r'(\.pyc?)?$') # Match .py or .pyc files, or module dirs + else: + MIGRATION_FILENAME = re.compile(r'(?!__init__)' # Don't match __init__.py + r'[0-9a-zA-Z_]*' # Don't match dotfiles, or names with dots/invalid chars in them + r'(\.py)?$') # Match only .py files, or module dirs + + def __init__(self, application, force_creation=False, verbose_creation=True): + "Constructor. Takes the module of the app, NOT its models (like get_app returns)" + self._cache = {} + self.set_application(application, force_creation, verbose_creation) + + def create_migrations_directory(self, verbose=True): + "Given an application, ensures that the migrations directory is ready." + migrations_dir = self.migrations_dir() + # Make the directory if it's not already there + if not os.path.isdir(migrations_dir): + if verbose: + print "Creating migrations directory at '%s'..." % migrations_dir + os.mkdir(migrations_dir) + # Same for __init__.py + init_path = os.path.join(migrations_dir, "__init__.py") + if not os.path.isfile(init_path): + # Touch the init py file + if verbose: + print "Creating __init__.py in '%s'..." % migrations_dir + open(init_path, "w").close() + + def migrations_dir(self): + """ + Returns the full path of the migrations directory. + If it doesn't exist yet, returns where it would exist, based on the + app's migrations module (defaults to app.migrations) + """ + module_path = self.migrations_module() + try: + module = importlib.import_module(module_path) + except ImportError: + # There's no migrations module made yet; guess! + try: + parent = importlib.import_module(".".join(module_path.split(".")[:-1])) + except ImportError: + # The parent doesn't even exist, that's an issue. + raise exceptions.InvalidMigrationModule( + application = self.application.__name__, + module = module_path, + ) + else: + # Good guess. + return os.path.join(os.path.dirname(parent.__file__), module_path.split(".")[-1]) + else: + # Get directory directly + return os.path.dirname(module.__file__) + + def migrations_module(self): + "Returns the module name of the migrations module for this" + app_label = application_to_app_label(self.application) + if hasattr(settings, "SOUTH_MIGRATION_MODULES"): + if app_label in settings.SOUTH_MIGRATION_MODULES: + # There's an override. + return settings.SOUTH_MIGRATION_MODULES[app_label] + return self._application.__name__ + '.migrations' + + def get_application(self): + return self._application + + def set_application(self, application, force_creation=False, verbose_creation=True): + """ + Called when the application for this Migrations is set. + Imports the migrations module object, and throws a paddy if it can't. + """ + self._application = application + if not hasattr(application, 'migrations'): + try: + module = importlib.import_module(self.migrations_module()) + self._migrations = application.migrations = module + except ImportError: + if force_creation: + self.create_migrations_directory(verbose_creation) + module = importlib.import_module(self.migrations_module()) + self._migrations = application.migrations = module + else: + raise exceptions.NoMigrations(application) + self._load_migrations_module(application.migrations) + + application = property(get_application, set_application) + + def _load_migrations_module(self, module): + self._migrations = module + filenames = [] + dirname = self.migrations_dir() + for f in os.listdir(dirname): + if self.MIGRATION_FILENAME.match(os.path.basename(f)): + full_path = os.path.join(dirname, f) + # If it's a .pyc file, only append if the .py isn't already around + if f.endswith(".pyc") and (os.path.isfile(full_path[:-1])): + continue + # If it's a module directory, only append if it contains __init__.py[c]. + if os.path.isdir(full_path): + if not (os.path.isfile(os.path.join(full_path, "__init__.py")) or \ + (getattr(settings, "SOUTH_USE_PYC", False) and \ + os.path.isfile(os.path.join(full_path, "__init__.pyc")))): + continue + filenames.append(f) + filenames.sort() + self.extend(self.migration(f) for f in filenames) + + def migration(self, filename): + name = Migration.strip_filename(filename) + if name not in self._cache: + self._cache[name] = Migration(self, name) + return self._cache[name] + + def __getitem__(self, value): + if isinstance(value, basestring): + return self.migration(value) + return super(Migrations, self).__getitem__(value) + + def _guess_migration(self, prefix): + prefix = Migration.strip_filename(prefix) + matches = [m for m in self if m.name().startswith(prefix)] + if len(matches) == 1: + return matches[0] + elif len(matches) > 1: + raise exceptions.MultiplePrefixMatches(prefix, matches) + else: + raise exceptions.UnknownMigration(prefix, None) + + def guess_migration(self, target_name): + if target_name == 'zero' or not self: + return + elif target_name is None: + return self[-1] + else: + return self._guess_migration(prefix=target_name) + + def app_label(self): + return self._application.__name__.split('.')[-1] + + def full_name(self): + return self._migrations.__name__ + + @classmethod + def calculate_dependencies(cls, force=False): + "Goes through all the migrations, and works out the dependencies." + if getattr(cls, "_dependencies_done", False) and not force: + return + for migrations in all_migrations(): + for migration in migrations: + migration.calculate_dependencies() + cls._dependencies_done = True + + @staticmethod + def invalidate_all_modules(): + "Goes through all the migrations, and invalidates all cached modules." + for migrations in all_migrations(): + for migration in migrations: + migration.invalidate_module() + + def next_filename(self, name): + "Returns the fully-formatted filename of what a new migration 'name' would be" + highest_number = 0 + for migration in self: + try: + number = int(migration.name().split("_")[0]) + highest_number = max(highest_number, number) + except ValueError: + pass + # Work out the new filename + return "%04i_%s.py" % ( + highest_number + 1, + name, + ) + + +class Migration(object): + + """ + Class which represents a particular migration file on-disk. + """ + + def __init__(self, migrations, filename): + """ + Returns the migration class implied by 'filename'. + """ + self.migrations = migrations + self.filename = filename + self.dependencies = set() + self.dependents = set() + + def __str__(self): + return self.app_label() + ':' + self.name() + + def __repr__(self): + return u'' % unicode(self) + + def __eq__(self, other): + return self.app_label() == other.app_label() and self.name() == other.name() + + def __hash__(self): + return hash(str(self)) + + def app_label(self): + return self.migrations.app_label() + + @staticmethod + def strip_filename(filename): + return os.path.splitext(os.path.basename(filename))[0] + + def name(self): + return self.strip_filename(os.path.basename(self.filename)) + + def full_name(self): + return self.migrations.full_name() + '.' + self.name() + + def migration(self): + "Tries to load the actual migration module" + full_name = self.full_name() + try: + migration = sys.modules[full_name] + except KeyError: + try: + migration = __import__(full_name, {}, {}, ['Migration']) + except ImportError, e: + raise exceptions.UnknownMigration(self, sys.exc_info()) + except Exception, e: + raise exceptions.BrokenMigration(self, sys.exc_info()) + # Override some imports + migration._ = lambda x: x # Fake i18n + migration.datetime = datetime_utils + return migration + migration = memoize(migration) + + def migration_class(self): + "Returns the Migration class from the module" + return self.migration().Migration + + def migration_instance(self): + "Instantiates the migration_class" + return self.migration_class()() + migration_instance = memoize(migration_instance) + + def previous(self): + "Returns the migration that comes before this one in the sequence." + index = self.migrations.index(self) - 1 + if index < 0: + return None + return self.migrations[index] + previous = memoize(previous) + + def next(self): + "Returns the migration that comes after this one in the sequence." + index = self.migrations.index(self) + 1 + if index >= len(self.migrations): + return None + return self.migrations[index] + next = memoize(next) + + def _get_dependency_objects(self, attrname): + """ + Given the name of an attribute (depends_on or needed_by), either yields + a list of migration objects representing it, or errors out. + """ + for app, name in getattr(self.migration_class(), attrname, []): + try: + migrations = Migrations(app) + except ImproperlyConfigured: + raise exceptions.DependsOnUnmigratedApplication(self, app) + migration = migrations.migration(name) + try: + migration.migration() + except exceptions.UnknownMigration: + raise exceptions.DependsOnUnknownMigration(self, migration) + if migration.is_before(self) == False: + raise exceptions.DependsOnHigherMigration(self, migration) + yield migration + + def calculate_dependencies(self): + """ + Loads dependency info for this migration, and stores it in itself + and any other relevant migrations. + """ + # Normal deps first + for migration in self._get_dependency_objects("depends_on"): + self.dependencies.add(migration) + migration.dependents.add(self) + # And reverse deps + for migration in self._get_dependency_objects("needed_by"): + self.dependents.add(migration) + migration.dependencies.add(self) + # And implicit ordering deps + previous = self.previous() + if previous: + self.dependencies.add(previous) + previous.dependents.add(self) + + def invalidate_module(self): + """ + Removes the cached version of this migration's module import, so we + have to re-import it. Used when south.db.db changes. + """ + reload(self.migration()) + self.migration._invalidate() + + def forwards(self): + return self.migration_instance().forwards + + def backwards(self): + return self.migration_instance().backwards + + def forwards_plan(self): + """ + Returns a list of Migration objects to be applied, in order. + + This list includes `self`, which will be applied last. + """ + return depends(self, lambda x: x.dependencies) + + def _backwards_plan(self): + return depends(self, lambda x: x.dependents) + + def backwards_plan(self): + """ + Returns a list of Migration objects to be unapplied, in order. + + This list includes `self`, which will be unapplied last. + """ + return list(self._backwards_plan()) + + def is_before(self, other): + if self.migrations == other.migrations: + if self.filename < other.filename: + return True + return False + + def is_after(self, other): + if self.migrations == other.migrations: + if self.filename > other.filename: + return True + return False + + def prev_orm(self): + if getattr(self.migration_class(), 'symmetrical', False): + return self.orm() + previous = self.previous() + if previous is None: + # First migration? The 'previous ORM' is empty. + return FakeORM(None, self.app_label()) + return previous.orm() + prev_orm = memoize(prev_orm) + + def orm(self): + return FakeORM(self.migration_class(), self.app_label()) + orm = memoize(orm) + + def no_dry_run(self): + migration_class = self.migration_class() + try: + return migration_class.no_dry_run + except AttributeError: + return False diff --git a/lib/python/south/migration/base.pyc b/lib/python/south/migration/base.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cad0783f5ee91f5fc8f128204d716bfb1acd8455 GIT binary patch literal 19326 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 PjF)kAr#f+D;`{#%s$a)` literal 0 HcmV?d00001 diff --git a/lib/python/south/migration/migrators.py b/lib/python/south/migration/migrators.py new file mode 100644 index 0000000..f289582 --- /dev/null +++ b/lib/python/south/migration/migrators.py @@ -0,0 +1,360 @@ +from copy import copy, deepcopy +from cStringIO import StringIO +import datetime +import inspect +import sys +import traceback + +from django.core.management import call_command +from django.core.management.commands import loaddata +from django.db import models + +import south.db +from south import exceptions +from south.db import DEFAULT_DB_ALIAS +from south.models import MigrationHistory +from south.signals import ran_migration + + +class Migrator(object): + def __init__(self, verbosity=0, interactive=False): + self.verbosity = int(verbosity) + self.interactive = bool(interactive) + + @staticmethod + def title(target): + raise NotImplementedError() + + def print_title(self, target): + if self.verbosity: + print self.title(target) + + @staticmethod + def status(target): + raise NotImplementedError() + + def print_status(self, migration): + status = self.status(migration) + if self.verbosity and status: + print status + + @staticmethod + def orm(migration): + raise NotImplementedError() + + def backwards(self, migration): + return self._wrap_direction(migration.backwards(), migration.prev_orm()) + + def direction(self, migration): + raise NotImplementedError() + + @staticmethod + def _wrap_direction(direction, orm): + args = inspect.getargspec(direction) + if len(args[0]) == 1: + # Old migration, no ORM should be passed in + return direction + return (lambda: direction(orm)) + + @staticmethod + def record(migration, database): + raise NotImplementedError() + + def run_migration_error(self, migration, extra_info=''): + return ( + ' ! Error found during real run of migration! Aborting.\n' + '\n' + ' ! Since you have a database that does not support running\n' + ' ! schema-altering statements in transactions, we have had \n' + ' ! to leave it in an interim state between migrations.\n' + '%s\n' + ' ! The South developers regret this has happened, and would\n' + ' ! like to gently persuade you to consider a slightly\n' + ' ! easier-to-deal-with DBMS (one that supports DDL transactions)\n' + ' ! NOTE: The error which caused the migration to fail is further up.' + ) % extra_info + + def run_migration(self, migration): + migration_function = self.direction(migration) + south.db.db.start_transaction() + try: + migration_function() + south.db.db.execute_deferred_sql() + except: + south.db.db.rollback_transaction() + if not south.db.db.has_ddl_transactions: + print self.run_migration_error(migration) + print "Error in migration: %s" % migration + raise + else: + try: + south.db.db.commit_transaction() + except: + print "Error during commit in migration: %s" % migration + raise + + + def run(self, migration): + # Get the correct ORM. + south.db.db.current_orm = self.orm(migration) + # If we're not already in a dry run, and the database doesn't support + # running DDL inside a transaction, *cough*MySQL*cough* then do a dry + # run first. + if not isinstance(getattr(self, '_wrapper', self), DryRunMigrator): + if not south.db.db.has_ddl_transactions: + dry_run = DryRunMigrator(migrator=self, ignore_fail=False) + dry_run.run_migration(migration) + return self.run_migration(migration) + + def done_migrate(self, migration, database): + south.db.db.start_transaction() + try: + # Record us as having done this + self.record(migration, database) + except: + south.db.db.rollback_transaction() + raise + else: + south.db.db.commit_transaction() + + def send_ran_migration(self, migration): + ran_migration.send(None, + app=migration.app_label(), + migration=migration, + method=self.__class__.__name__.lower()) + + def migrate(self, migration, database): + """ + Runs the specified migration forwards/backwards, in order. + """ + app = migration.migrations._migrations + migration_name = migration.name() + self.print_status(migration) + result = self.run(migration) + self.done_migrate(migration, database) + self.send_ran_migration(migration) + return result + + def migrate_many(self, target, migrations, database): + raise NotImplementedError() + + +class MigratorWrapper(object): + def __init__(self, migrator, *args, **kwargs): + self._migrator = copy(migrator) + attributes = dict([(k, getattr(self, k)) + for k in self.__class__.__dict__.iterkeys() + if not k.startswith('__')]) + self._migrator.__dict__.update(attributes) + self._migrator.__dict__['_wrapper'] = self + + def __getattr__(self, name): + return getattr(self._migrator, name) + + +class DryRunMigrator(MigratorWrapper): + def __init__(self, ignore_fail=True, *args, **kwargs): + super(DryRunMigrator, self).__init__(*args, **kwargs) + self._ignore_fail = ignore_fail + + def _run_migration(self, migration): + if migration.no_dry_run(): + if self.verbosity: + print " - Migration '%s' is marked for no-dry-run." % migration + return + south.db.db.dry_run = True + # preserve the constraint cache as it can be mutated by the dry run + constraint_cache = deepcopy(south.db.db._constraint_cache) + if self._ignore_fail: + south.db.db.debug, old_debug = False, south.db.db.debug + pending_creates = south.db.db.get_pending_creates() + south.db.db.start_transaction() + migration_function = self.direction(migration) + try: + try: + migration_function() + south.db.db.execute_deferred_sql() + except: + raise exceptions.FailedDryRun(migration, sys.exc_info()) + finally: + south.db.db.rollback_transactions_dry_run() + if self._ignore_fail: + south.db.db.debug = old_debug + south.db.db.clear_run_data(pending_creates) + south.db.db.dry_run = False + # restore the preserved constraint cache from before dry run was + # executed + south.db.db._constraint_cache = constraint_cache + + def run_migration(self, migration): + try: + self._run_migration(migration) + except exceptions.FailedDryRun: + if self._ignore_fail: + return False + raise + + def done_migrate(self, *args, **kwargs): + pass + + def send_ran_migration(self, *args, **kwargs): + pass + + +class FakeMigrator(MigratorWrapper): + def run(self, migration): + if self.verbosity: + print ' (faked)' + + def send_ran_migration(self, *args, **kwargs): + pass + + +class LoadInitialDataMigrator(MigratorWrapper): + + def load_initial_data(self, target, db='default'): + if target is None or target != target.migrations[-1]: + return + # Load initial data, if we ended up at target + if self.verbosity: + print " - Loading initial data for %s." % target.app_label() + # Override Django's get_apps call temporarily to only load from the + # current app + old_get_apps = models.get_apps + new_get_apps = lambda: [models.get_app(target.app_label())] + models.get_apps = new_get_apps + loaddata.get_apps = new_get_apps + try: + call_command('loaddata', 'initial_data', verbosity=self.verbosity, database=db) + finally: + models.get_apps = old_get_apps + loaddata.get_apps = old_get_apps + + def migrate_many(self, target, migrations, database): + migrator = self._migrator + result = migrator.__class__.migrate_many(migrator, target, migrations, database) + if result: + self.load_initial_data(target, db=database) + return True + + +class Forwards(Migrator): + """ + Runs the specified migration forwards, in order. + """ + torun = 'forwards' + + @staticmethod + def title(target): + if target is not None: + return " - Migrating forwards to %s." % target.name() + else: + assert False, "You cannot migrate forwards to zero." + + @staticmethod + def status(migration): + return ' > %s' % migration + + @staticmethod + def orm(migration): + return migration.orm() + + def forwards(self, migration): + return self._wrap_direction(migration.forwards(), migration.orm()) + + direction = forwards + + @staticmethod + def record(migration, database): + # Record us as having done this + record = MigrationHistory.for_migration(migration, database) + try: + from django.utils.timezone import now + record.applied = now() + except ImportError: + record.applied = datetime.datetime.utcnow() + if database != DEFAULT_DB_ALIAS: + record.save(using=database) + else: + # Django 1.1 and below always go down this branch. + record.save() + + def format_backwards(self, migration): + if migration.no_dry_run(): + return " (migration cannot be dry-run; cannot discover commands)" + old_debug, old_dry_run = south.db.db.debug, south.db.db.dry_run + south.db.db.debug = south.db.db.dry_run = True + stdout = sys.stdout + sys.stdout = StringIO() + try: + try: + self.backwards(migration)() + return sys.stdout.getvalue() + except: + raise + finally: + south.db.db.debug, south.db.db.dry_run = old_debug, old_dry_run + sys.stdout = stdout + + def run_migration_error(self, migration, extra_info=''): + extra_info = ('\n' + '! You *might* be able to recover with:' + '%s' + '%s' % + (self.format_backwards(migration), extra_info)) + return super(Forwards, self).run_migration_error(migration, extra_info) + + def migrate_many(self, target, migrations, database): + try: + for migration in migrations: + result = self.migrate(migration, database) + if result is False: # The migrations errored, but nicely. + return False + finally: + # Call any pending post_syncdb signals + south.db.db.send_pending_create_signals(verbosity=self.verbosity, + interactive=self.interactive) + return True + + +class Backwards(Migrator): + """ + Runs the specified migration backwards, in order. + """ + torun = 'backwards' + + @staticmethod + def title(target): + if target is None: + return " - Migrating backwards to zero state." + else: + return " - Migrating backwards to just after %s." % target.name() + + @staticmethod + def status(migration): + return ' < %s' % migration + + @staticmethod + def orm(migration): + return migration.prev_orm() + + direction = Migrator.backwards + + @staticmethod + def record(migration, database): + # Record us as having not done this + record = MigrationHistory.for_migration(migration, database) + if record.id is not None: + if database != DEFAULT_DB_ALIAS: + record.delete(using=database) + else: + # Django 1.1 always goes down here + record.delete() + + def migrate_many(self, target, migrations, database): + for migration in migrations: + self.migrate(migration, database) + return True + + + diff --git a/lib/python/south/migration/migrators.pyc b/lib/python/south/migration/migrators.pyc new file mode 100644 index 0000000000000000000000000000000000000000..af4ed5f69337f79352bfd890ca8f75a0fb2b7569 GIT binary patch literal 15677 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 literal 0 HcmV?d00001 diff --git a/lib/python/south/migration/utils.py b/lib/python/south/migration/utils.py new file mode 100644 index 0000000..da37397 --- /dev/null +++ b/lib/python/south/migration/utils.py @@ -0,0 +1,83 @@ +import sys +from collections import deque + +from django.utils.datastructures import SortedDict +from django.db import models + +from south import exceptions + + +class SortedSet(SortedDict): + def __init__(self, data=tuple()): + self.extend(data) + + def __str__(self): + return "SortedSet(%s)" % list(self) + + def add(self, value): + self[value] = True + + def remove(self, value): + del self[value] + + def extend(self, iterable): + [self.add(k) for k in iterable] + + +def get_app_label(app): + """ + Returns the _internal_ app label for the given app module. + i.e. for will return 'auth' + """ + return app.__name__.split('.')[-2] + + +def app_label_to_app_module(app_label): + """ + Given the app label, returns the module of the app itself (unlike models.get_app, + which returns the models module) + """ + # Get the models module + app = models.get_app(app_label) + module_name = ".".join(app.__name__.split(".")[:-1]) + try: + module = sys.modules[module_name] + except KeyError: + __import__(module_name, {}, {}, ['']) + module = sys.modules[module_name] + return module + + +def flatten(*stack): + stack = deque(stack) + while stack: + try: + x = stack[0].next() + except AttributeError: + stack[0] = iter(stack[0]) + x = stack[0].next() + except StopIteration: + stack.popleft() + continue + if hasattr(x, '__iter__'): + stack.appendleft(x) + else: + yield x + +def _dfs(start, get_children, path): + if start in path: + raise exceptions.CircularDependency(path[path.index(start):] + [start]) + path.append(start) + yield start + children = sorted(get_children(start), key=lambda x: str(x)) + if children: + # We need to apply all the migrations this one depends on + yield (_dfs(n, get_children, path) for n in children) + path.pop() + +def dfs(start, get_children): + return flatten(_dfs(start, get_children, [])) + +def depends(start, get_children): + result = SortedSet(reversed(list(dfs(start, get_children)))) + return list(result) diff --git a/lib/python/south/migration/utils.pyc b/lib/python/south/migration/utils.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bc6941e57c867cb10486b13800162c1f6b0313d4 GIT binary patch literal 4340 zcmcInU2hc06}>e*9)DnC$g&`Uvb-( z|2ufxKTzcOGg69dhk8RwBZG!)H^gu(k}{G(Q?{F--IQ`c1})ich4zA!i!xY}?IpBZ zVwK6#uJ|5f9TtTxpti8?e6`2oxE%XhBxh>tv9DA7AkuQdo^UJd(J?m$TG|>A? zJnk1LEWjGE4dMp_FO6(RghMBqk~g%nKxWv}lDw&vMTvVI0?cS_`rM_}^C+H)^idi) zHcDJi;Z6J^XnW{wm3O$%H@V7|yIRRppLWn8S~yzV_^q?G+t{hoDj!xGy~10!;m3zI zAKomBoee&tBfpO^Hyr!@jX|+jr(|Gb?2FQMF}MUfNm5jWPm(jJJHA;=MxyydcoaQ@ z0QbH60Q47o3UbS>VIa^CE;~PQ9|gtlCU=>`7Xp4_$MnoT$<~rySk|OaRI@LjkP-?X5S^qEk*l#;a^< zAB(w3g0ZUYVEDvdpxKEa_>ag*mO}h7Dtij?-lO;Bd1I<=>=+||Y{=uq|5JEOt#oK5 zuuSnmv~{{uT2Hkd%-@}uoADB!i6b4pbRzpI9oQuCOdv^ubTKa3B!NK2#eT)^711WbdF{%3djUqq3M}YbpYuPH3GMqn?7> zvp#|KL|wjuj-Z4ErInO3;40}qqk=R6PC+hI{9MWHL)u3Ak@S)Gr(Cx=+>oR5bf({u zXQ2OiBoC|4rRf*tU`6V&7-OI`3>DD1o}znLk0bQ0HI9k zVLXdZe35pT{=)i%d(=;ka{xRAjW9XP-kr8C1&DlYr<3e}CjOWgG)-K^KWywvh}~a|R5~5$A`CK44-5kLdMXY` zAzUAijX}!5)~{d4DFrJRuiDM$%+_Tsgqx$xJ zH1sHGNrwq4Cc*v(DDD<@HZIfpo~7Pxl^vRi|Gr8c%GSkP(?woKI^X ze?hyS(=M5P%c*dj&L+&u$M_Sx=q{d+_pn!fiXF0Gu9#NiikW@Y%+5^B*TAQF3vom; zL=d6fsrDGG_$CWl4ygHorM_u+<{Beq5=z;^E8e}kGD_;&&rpHv02)~sE1*R{N8SYP z@Q1ShSmF^g|ZBH}wJPJEtke3xK*av{A" % (self.app_name, self.migration) diff --git a/lib/python/south/models.pyc b/lib/python/south/models.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1fabc7e8fb880a6f35eaeab8eb01ea080c582fcd GIT binary patch literal 1767 zcmcIl!A{#i5S_J?Ktd=f1#XpEiAuSY^i~N`R3M;Mf(SXFw&}rg;!Wb<*vPw)Knj;q zsUO@Apl`+jimIMUlk9kB*4};d=8fb2T&%qHzaIB!_Hl82k73@yg!n0niN=aLiWK_} z#SV>K>bUS-(uzg}>MIHh^quIM=u4+aVUgPP5_X7g@Tn`zEW6`0)UnC^8vN$|!OrR7 ziQnAycMd=9v@HjZqCpneC`~>@#-`cj18lpv!3<$EF(mEK$erQ5Ce!$BN-txl82(H@Ch1$m*+*M^qQRZ zr{oP4c#{&HRVe#LN-4Og&tD(;91_aF;97PbHaI``lVGfE8P52YWl7MJ5^<_&ccgpP zNOltwAzj{JpshT`Tf{+E$CmSz&8nC*Q*BzQ-M>J^>bxQk^2Pjl4Tc=osXD7}S(Q~)J#}iTglmrzb!KA^SMwYOEV8gg=vNRrC}papQZojBQ! literal 0 HcmV?d00001 diff --git a/lib/python/south/modelsinspector.py b/lib/python/south/modelsinspector.py new file mode 100644 index 0000000..9b47690 --- /dev/null +++ b/lib/python/south/modelsinspector.py @@ -0,0 +1,462 @@ +""" +Like the old south.modelsparser, but using introspection where possible +rather than direct inspection of models.py. +""" + +import datetime +import re +import decimal + +from south.utils import get_attribute, auto_through + +from django.db import models +from django.db.models.base import ModelBase, Model +from django.db.models.fields import NOT_PROVIDED +from django.conf import settings +from django.utils.functional import Promise +from django.contrib.contenttypes import generic +from django.utils.datastructures import SortedDict +from django.utils import datetime_safe + +NOISY = False + +try: + from django.utils import timezone +except ImportError: + timezone = False + + +# Define any converter functions first to prevent NameErrors + +def convert_on_delete_handler(value): + django_db_models_module = 'models' # relative to standard import 'django.db' + if hasattr(models, "PROTECT"): + if value in (models.CASCADE, models.PROTECT, models.DO_NOTHING, models.SET_DEFAULT): + # straightforward functions + return '%s.%s' % (django_db_models_module, value.__name__) + else: + # This is totally dependent on the implementation of django.db.models.deletion.SET + func_name = getattr(value, '__name__', None) + if func_name == 'set_on_delete': + # we must inspect the function closure to see what parameters were passed in + closure_contents = value.func_closure[0].cell_contents + if closure_contents is None: + return "%s.SET_NULL" % (django_db_models_module) + # simple function we can perhaps cope with: + elif hasattr(closure_contents, '__call__'): + raise ValueError("South does not support on_delete with SET(function) as values.") + else: + # Attempt to serialise the value + return "%s.SET(%s)" % (django_db_models_module, value_clean(closure_contents)) + raise ValueError("%s was not recognized as a valid model deletion handler. Possible values: %s." % (value, ', '.join(f.__name__ for f in (models.CASCADE, models.PROTECT, models.SET, models.SET_NULL, models.SET_DEFAULT, models.DO_NOTHING)))) + else: + raise ValueError("on_delete argument encountered in Django version that does not support it") + +# Gives information about how to introspect certain fields. +# This is a list of triples; the first item is a list of fields it applies to, +# (note that isinstance is used, so superclasses are perfectly valid here) +# the second is a list of positional argument descriptors, and the third +# is a list of keyword argument descriptors. +# Descriptors are of the form: +# [attrname, options] +# Where attrname is the attribute on the field to get the value from, and options +# is an optional dict. +# +# The introspector uses the combination of all matching entries, in order. + +introspection_details = [ + ( + (models.Field, ), + [], + { + "null": ["null", {"default": False}], + "blank": ["blank", {"default": False, "ignore_if":"primary_key"}], + "primary_key": ["primary_key", {"default": False}], + "max_length": ["max_length", {"default": None}], + "unique": ["_unique", {"default": False}], + "db_index": ["db_index", {"default": False}], + "default": ["default", {"default": NOT_PROVIDED, "ignore_dynamics": True}], + "db_column": ["db_column", {"default": None}], + "db_tablespace": ["db_tablespace", {"default": settings.DEFAULT_INDEX_TABLESPACE}], + }, + ), + ( + (models.ForeignKey, models.OneToOneField), + [], + dict([ + ("to", ["rel.to", {}]), + ("to_field", ["rel.field_name", {"default_attr": "rel.to._meta.pk.name"}]), + ("related_name", ["rel.related_name", {"default": None}]), + ("db_index", ["db_index", {"default": True}]), + ("on_delete", ["rel.on_delete", {"default": getattr(models, "CASCADE", None), "is_django_function": True, "converter": convert_on_delete_handler, "ignore_missing": True}]) + ]) + ), + ( + (models.ManyToManyField,), + [], + { + "to": ["rel.to", {}], + "symmetrical": ["rel.symmetrical", {"default": True}], + "related_name": ["rel.related_name", {"default": None}], + "db_table": ["db_table", {"default": None}], + # TODO: Kind of ugly to add this one-time-only option + "through": ["rel.through", {"ignore_if_auto_through": True}], + }, + ), + ( + (models.DateField, models.TimeField), + [], + { + "auto_now": ["auto_now", {"default": False}], + "auto_now_add": ["auto_now_add", {"default": False}], + }, + ), + ( + (models.DecimalField, ), + [], + { + "max_digits": ["max_digits", {"default": None}], + "decimal_places": ["decimal_places", {"default": None}], + }, + ), + ( + (models.SlugField, ), + [], + { + "db_index": ["db_index", {"default": True}], + }, + ), + ( + (models.BooleanField, ), + [], + { + "default": ["default", {"default": NOT_PROVIDED, "converter": bool}], + "blank": ["blank", {"default": True, "ignore_if":"primary_key"}], + }, + ), + ( + (models.FilePathField, ), + [], + { + "path": ["path", {"default": ''}], + "match": ["match", {"default": None}], + "recursive": ["recursive", {"default": False}], + }, + ), + ( + (generic.GenericRelation, ), + [], + { + "to": ["rel.to", {}], + "symmetrical": ["rel.symmetrical", {"default": True}], + "object_id_field": ["object_id_field_name", {"default": "object_id"}], + "content_type_field": ["content_type_field_name", {"default": "content_type"}], + "blank": ["blank", {"default": True}], + }, + ), +] + +# Regexes of allowed field full paths +allowed_fields = [ + "^django\.db", + "^django\.contrib\.contenttypes\.generic", + "^django\.contrib\.localflavor", +] + +# Regexes of ignored fields (custom fields which look like fields, but have no column behind them) +ignored_fields = [ + "^django\.contrib\.contenttypes\.generic\.GenericRelation", + "^django\.contrib\.contenttypes\.generic\.GenericForeignKey", +] + +# Similar, but for Meta, so just the inner level (kwds). +meta_details = { + "db_table": ["db_table", {"default_attr_concat": ["%s_%s", "app_label", "module_name"]}], + "db_tablespace": ["db_tablespace", {"default": settings.DEFAULT_TABLESPACE}], + "unique_together": ["unique_together", {"default": []}], + "ordering": ["ordering", {"default": []}], + "proxy": ["proxy", {"default": False, "ignore_missing": True}], +} + +# 2.4 compatability +any = lambda x: reduce(lambda y, z: y or z, x, False) + + +def add_introspection_rules(rules=[], patterns=[]): + "Allows you to add some introspection rules at runtime, e.g. for 3rd party apps." + assert isinstance(rules, (list, tuple)) + assert isinstance(patterns, (list, tuple)) + allowed_fields.extend(patterns) + introspection_details.extend(rules) + + +def add_ignored_fields(patterns): + "Allows you to add some ignore field patterns." + assert isinstance(patterns, (list, tuple)) + ignored_fields.extend(patterns) + + +def can_ignore(field): + """ + Returns True if we know for certain that we can ignore this field, False + otherwise. + """ + full_name = "%s.%s" % (field.__class__.__module__, field.__class__.__name__) + for regex in ignored_fields: + if re.match(regex, full_name): + return True + return False + + +def can_introspect(field): + """ + Returns True if we are allowed to introspect this field, False otherwise. + ('allowed' means 'in core'. Custom fields can declare they are introspectable + by the default South rules by adding the attribute _south_introspects = True.) + """ + # Check for special attribute + if hasattr(field, "_south_introspects") and field._south_introspects: + return True + # Check it's an introspectable field + full_name = "%s.%s" % (field.__class__.__module__, field.__class__.__name__) + for regex in allowed_fields: + if re.match(regex, full_name): + return True + return False + + +def matching_details(field): + """ + Returns the union of all matching entries in introspection_details for the field. + """ + our_args = [] + our_kwargs = {} + for classes, args, kwargs in introspection_details: + if any([isinstance(field, x) for x in classes]): + our_args.extend(args) + our_kwargs.update(kwargs) + return our_args, our_kwargs + + +class IsDefault(Exception): + """ + Exception for when a field contains its default value. + """ + + +def get_value(field, descriptor): + """ + Gets an attribute value from a Field instance and formats it. + """ + attrname, options = descriptor + # If the options say it's not a attribute name but a real value, use that. + if options.get('is_value', False): + value = attrname + else: + try: + value = get_attribute(field, attrname) + except AttributeError: + if options.get("ignore_missing", False): + raise IsDefault + else: + raise + + # Lazy-eval functions get eval'd. + if isinstance(value, Promise): + value = unicode(value) + # If the value is the same as the default, omit it for clarity + if "default" in options and value == options['default']: + raise IsDefault + # If there's an ignore_if, use it + if "ignore_if" in options: + if get_attribute(field, options['ignore_if']): + raise IsDefault + # If there's an ignore_if_auto_through which is True, use it + if options.get("ignore_if_auto_through", False): + if auto_through(field): + raise IsDefault + # Some default values need to be gotten from an attribute too. + if "default_attr" in options: + default_value = get_attribute(field, options['default_attr']) + if value == default_value: + raise IsDefault + # Some are made from a formatting string and several attrs (e.g. db_table) + if "default_attr_concat" in options: + format, attrs = options['default_attr_concat'][0], options['default_attr_concat'][1:] + default_value = format % tuple(map(lambda x: get_attribute(field, x), attrs)) + if value == default_value: + raise IsDefault + # Clean and return the value + return value_clean(value, options) + + +def value_clean(value, options={}): + "Takes a value and cleans it up (so e.g. it has timezone working right)" + # Lazy-eval functions get eval'd. + if isinstance(value, Promise): + value = unicode(value) + # Callables get called. + if not options.get('is_django_function', False) and callable(value) and not isinstance(value, ModelBase): + # Datetime.datetime.now is special, as we can access it from the eval + # context (and because it changes all the time; people will file bugs otherwise). + if value == datetime.datetime.now: + return "datetime.datetime.now" + elif value == datetime.datetime.utcnow: + return "datetime.datetime.utcnow" + elif value == datetime.date.today: + return "datetime.date.today" + # In case we use Django's own now function, revert to datetime's + # original one since we'll deal with timezones on our own. + elif timezone and value == timezone.now: + return "datetime.datetime.now" + # All other callables get called. + value = value() + # Models get their own special repr() + if isinstance(value, ModelBase): + # If it's a proxy model, follow it back to its non-proxy parent + if getattr(value._meta, "proxy", False): + value = value._meta.proxy_for_model + return "orm['%s.%s']" % (value._meta.app_label, value._meta.object_name) + # As do model instances + if isinstance(value, Model): + if options.get("ignore_dynamics", False): + raise IsDefault + return "orm['%s.%s'].objects.get(pk=%r)" % (value.__class__._meta.app_label, value.__class__._meta.object_name, value.pk) + # Make sure Decimal is converted down into a string + if isinstance(value, decimal.Decimal): + value = str(value) + # in case the value is timezone aware + datetime_types = ( + datetime.datetime, + datetime.time, + datetime_safe.datetime, + ) + if (timezone and isinstance(value, datetime_types) and + getattr(settings, 'USE_TZ', False) and + value is not None and timezone.is_aware(value)): + default_timezone = timezone.get_default_timezone() + value = timezone.make_naive(value, default_timezone) + # datetime_safe has an improper repr value + if isinstance(value, datetime_safe.datetime): + value = datetime.datetime(*value.utctimetuple()[:7]) + # converting a date value to a datetime to be able to handle + # timezones later gracefully + elif isinstance(value, (datetime.date, datetime_safe.date)): + value = datetime.datetime(*value.timetuple()[:3]) + # Now, apply the converter func if there is one + if "converter" in options: + value = options['converter'](value) + # Return the final value + if options.get('is_django_function', False): + return value + else: + return repr(value) + + +def introspector(field): + """ + Given a field, introspects its definition triple. + """ + arg_defs, kwarg_defs = matching_details(field) + args = [] + kwargs = {} + # For each argument, use the descriptor to get the real value. + for defn in arg_defs: + try: + args.append(get_value(field, defn)) + except IsDefault: + pass + for kwd, defn in kwarg_defs.items(): + try: + kwargs[kwd] = get_value(field, defn) + except IsDefault: + pass + return args, kwargs + + +def get_model_fields(model, m2m=False): + """ + Given a model class, returns a dict of {field_name: field_triple} defs. + """ + + field_defs = SortedDict() + inherited_fields = {} + + # Go through all bases (that are themselves models, but not Model) + for base in model.__bases__: + if hasattr(base, '_meta') and issubclass(base, models.Model): + if not base._meta.abstract: + # Looks like we need their fields, Ma. + inherited_fields.update(get_model_fields(base)) + + # Now, go through all the fields and try to get their definition + source = model._meta.local_fields[:] + if m2m: + source += model._meta.local_many_to_many + + for field in source: + # Can we ignore it completely? + if can_ignore(field): + continue + # Does it define a south_field_triple method? + if hasattr(field, "south_field_triple"): + if NOISY: + print " ( Nativing field: %s" % field.name + field_defs[field.name] = field.south_field_triple() + # Can we introspect it? + elif can_introspect(field): + # Get the full field class path. + field_class = field.__class__.__module__ + "." + field.__class__.__name__ + # Run this field through the introspector + args, kwargs = introspector(field) + # Workaround for Django bug #13987 + if model._meta.pk.column == field.column and 'primary_key' not in kwargs: + kwargs['primary_key'] = True + # That's our definition! + field_defs[field.name] = (field_class, args, kwargs) + # Shucks, no definition! + else: + if NOISY: + print " ( Nodefing field: %s" % field.name + field_defs[field.name] = None + + # If they've used the horrific hack that is order_with_respect_to, deal with + # it. + if model._meta.order_with_respect_to: + field_defs['_order'] = ("django.db.models.fields.IntegerField", [], {"default": "0"}) + + return field_defs + + +def get_model_meta(model): + """ + Given a model class, will return the dict representing the Meta class. + """ + + # Get the introspected attributes + meta_def = {} + for kwd, defn in meta_details.items(): + try: + meta_def[kwd] = get_value(model._meta, defn) + except IsDefault: + pass + + # Also, add on any non-abstract model base classes. + # This is called _ormbases as the _bases variable was previously used + # for a list of full class paths to bases, so we can't conflict. + for base in model.__bases__: + if hasattr(base, '_meta') and issubclass(base, models.Model): + if not base._meta.abstract: + # OK, that matches our terms. + if "_ormbases" not in meta_def: + meta_def['_ormbases'] = [] + meta_def['_ormbases'].append("%s.%s" % ( + base._meta.app_label, + base._meta.object_name, + )) + + return meta_def + + +# Now, load the built-in South introspection plugins +import south.introspection_plugins diff --git a/lib/python/south/modelsinspector.pyc b/lib/python/south/modelsinspector.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ad856d9eeeb36e1d1272cb6af8a0e311b9d97223 GIT binary patch literal 13046 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 CSE{Q3 literal 0 HcmV?d00001 diff --git a/lib/python/south/orm.py b/lib/python/south/orm.py new file mode 100644 index 0000000..e51bc9e --- /dev/null +++ b/lib/python/south/orm.py @@ -0,0 +1,400 @@ +""" +South's fake ORM; lets you not have to write SQL inside migrations. +Roughly emulates the real Django ORM, to a point. +""" + +import inspect + +from django.db import models +from django.db.models.loading import cache +from django.core.exceptions import ImproperlyConfigured + +from south.db import db +from south.utils import ask_for_it_by_name, datetime_utils +from south.hacks import hacks +from south.exceptions import UnfreezeMeLater, ORMBaseNotIncluded, ImpossibleORMUnfreeze + + +class ModelsLocals(object): + + """ + Custom dictionary-like class to be locals(); + falls back to lowercase search for items that don't exist + (because we store model names as lowercase). + """ + + def __init__(self, data): + self.data = data + + def __getitem__(self, key): + try: + return self.data[key] + except KeyError: + return self.data[key.lower()] + + +# Stores already-created ORMs. +_orm_cache = {} + +def FakeORM(*args): + """ + Creates a Fake Django ORM. + This is actually a memoised constructor; the real class is _FakeORM. + """ + if not args in _orm_cache: + _orm_cache[args] = _FakeORM(*args) + return _orm_cache[args] + + +class LazyFakeORM(object): + """ + In addition to memoising the ORM call, this function lazily generates them + for a Migration class. Assign the result of this to (for example) + .orm, and as soon as .orm is accessed the ORM will be created. + """ + + def __init__(self, *args): + self._args = args + self.orm = None + + def __get__(self, obj, type=None): + if not self.orm: + self.orm = FakeORM(*self._args) + return self.orm + + +class _FakeORM(object): + + """ + Simulates the Django ORM at some point in time, + using a frozen definition on the Migration class. + """ + + def __init__(self, cls, app): + self.default_app = app + self.cls = cls + # Try loading the models off the migration class; default to no models. + self.models = {} + try: + self.models_source = cls.models + except AttributeError: + return + + # Start a 'new' AppCache + hacks.clear_app_cache() + + # Now, make each model's data into a FakeModel + # We first make entries for each model that are just its name + # This allows us to have circular model dependency loops + model_names = [] + for name, data in self.models_source.items(): + # Make sure there's some kind of Meta + if "Meta" not in data: + data['Meta'] = {} + try: + app_label, model_name = name.split(".", 1) + except ValueError: + app_label = self.default_app + model_name = name + + # If there's an object_name in the Meta, use it and remove it + if "object_name" in data['Meta']: + model_name = data['Meta']['object_name'] + del data['Meta']['object_name'] + + name = "%s.%s" % (app_label, model_name) + self.models[name.lower()] = name + model_names.append((name.lower(), app_label, model_name, data)) + + # Loop until model_names is entry, or hasn't shrunk in size since + # last iteration. + # The make_model method can ask to postpone a model; it's then pushed + # to the back of the queue. Because this is currently only used for + # inheritance, it should thus theoretically always decrease by one. + last_size = None + while model_names: + # First, make sure we've shrunk. + if len(model_names) == last_size: + raise ImpossibleORMUnfreeze() + last_size = len(model_names) + # Make one run through + postponed_model_names = [] + for name, app_label, model_name, data in model_names: + try: + self.models[name] = self.make_model(app_label, model_name, data) + except UnfreezeMeLater: + postponed_model_names.append((name, app_label, model_name, data)) + # Reset + model_names = postponed_model_names + + # And perform the second run to iron out any circular/backwards depends. + self.retry_failed_fields() + + # Force evaluation of relations on the models now + for model in self.models.values(): + model._meta.get_all_field_names() + + # Reset AppCache + hacks.unclear_app_cache() + + + def __iter__(self): + return iter(self.models.values()) + + + def __getattr__(self, key): + fullname = (self.default_app+"."+key).lower() + try: + return self.models[fullname] + except KeyError: + raise AttributeError("The model '%s' from the app '%s' is not available in this migration. (Did you use orm.ModelName, not orm['app.ModelName']?)" % (key, self.default_app)) + + + def __getitem__(self, key): + # Detect if they asked for a field on a model or not. + if ":" in key: + key, fname = key.split(":") + else: + fname = None + # Now, try getting the model + key = key.lower() + try: + model = self.models[key] + except KeyError: + try: + app, model = key.split(".", 1) + except ValueError: + raise KeyError("The model '%s' is not in appname.modelname format." % key) + else: + raise KeyError("The model '%s' from the app '%s' is not available in this migration." % (model, app)) + # If they asked for a field, get it. + if fname: + return model._meta.get_field_by_name(fname)[0] + else: + return model + + + def eval_in_context(self, code, app, extra_imports={}): + "Evaluates the given code in the context of the migration file." + + # Drag in the migration module's locals (hopefully including models.py) + fake_locals = dict(inspect.getmodule(self.cls).__dict__) + + # Remove all models from that (i.e. from modern models.py), to stop pollution + for key, value in fake_locals.items(): + if isinstance(value, type) and issubclass(value, models.Model) and hasattr(value, "_meta"): + del fake_locals[key] + + # We add our models into the locals for the eval + fake_locals.update(dict([ + (name.split(".")[-1], model) + for name, model in self.models.items() + ])) + + # Make sure the ones for this app override. + fake_locals.update(dict([ + (name.split(".")[-1], model) + for name, model in self.models.items() + if name.split(".")[0] == app + ])) + + # Ourselves as orm, to allow non-fail cross-app referencing + fake_locals['orm'] = self + + # And a fake _ function + fake_locals['_'] = lambda x: x + + # Datetime; there should be no datetime direct accesses + fake_locals['datetime'] = datetime_utils + + # Now, go through the requested imports and import them. + for name, value in extra_imports.items(): + # First, try getting it out of locals. + parts = value.split(".") + try: + obj = fake_locals[parts[0]] + for part in parts[1:]: + obj = getattr(obj, part) + except (KeyError, AttributeError): + pass + else: + fake_locals[name] = obj + continue + # OK, try to import it directly + try: + fake_locals[name] = ask_for_it_by_name(value) + except ImportError: + if name == "SouthFieldClass": + raise ValueError("Cannot import the required field '%s'" % value) + else: + print "WARNING: Cannot import '%s'" % value + + # Use ModelsLocals to make lookups work right for CapitalisedModels + fake_locals = ModelsLocals(fake_locals) + + return eval(code, globals(), fake_locals) + + + def make_meta(self, app, model, data, stub=False): + "Makes a Meta class out of a dict of eval-able arguments." + results = {'app_label': app} + for key, code in data.items(): + # Some things we never want to use. + if key in ["_bases", "_ormbases"]: + continue + # Some things we don't want with stubs. + if stub and key in ["order_with_respect_to"]: + continue + # OK, add it. + try: + results[key] = self.eval_in_context(code, app) + except (NameError, AttributeError), e: + raise ValueError("Cannot successfully create meta field '%s' for model '%s.%s': %s." % ( + key, app, model, e + )) + return type("Meta", tuple(), results) + + + def make_model(self, app, name, data): + "Makes a Model class out of the given app name, model name and pickled data." + + # Extract any bases out of Meta + if "_ormbases" in data['Meta']: + # Make sure everything we depend on is done already; otherwise, wait. + for key in data['Meta']['_ormbases']: + key = key.lower() + if key not in self.models: + raise ORMBaseNotIncluded("Cannot find ORM base %s" % key) + elif isinstance(self.models[key], basestring): + # Then the other model hasn't been unfrozen yet. + # We postpone ourselves; the situation will eventually resolve. + raise UnfreezeMeLater() + bases = [self.models[key.lower()] for key in data['Meta']['_ormbases']] + # Perhaps the old style? + elif "_bases" in data['Meta']: + bases = map(ask_for_it_by_name, data['Meta']['_bases']) + # Ah, bog standard, then. + else: + bases = [models.Model] + + # Turn the Meta dict into a basic class + meta = self.make_meta(app, name, data['Meta'], data.get("_stub", False)) + + failed_fields = {} + fields = {} + stub = False + + # Now, make some fields! + for fname, params in data.items(): + # If it's the stub marker, ignore it. + if fname == "_stub": + stub = bool(params) + continue + elif fname == "Meta": + continue + elif not params: + raise ValueError("Field '%s' on model '%s.%s' has no definition." % (fname, app, name)) + elif isinstance(params, (str, unicode)): + # It's a premade definition string! Let's hope it works... + code = params + extra_imports = {} + else: + # If there's only one parameter (backwards compat), make it 3. + if len(params) == 1: + params = (params[0], [], {}) + # There should be 3 parameters. Code is a tuple of (code, what-to-import) + if len(params) == 3: + code = "SouthFieldClass(%s)" % ", ".join( + params[1] + + ["%s=%s" % (n, v) for n, v in params[2].items()] + ) + extra_imports = {"SouthFieldClass": params[0]} + else: + raise ValueError("Field '%s' on model '%s.%s' has a weird definition length (should be 1 or 3 items)." % (fname, app, name)) + + try: + # Execute it in a probably-correct context. + field = self.eval_in_context(code, app, extra_imports) + except (NameError, AttributeError, AssertionError, KeyError): + # It might rely on other models being around. Add it to the + # model for the second pass. + failed_fields[fname] = (code, extra_imports) + else: + fields[fname] = field + + # Find the app in the Django core, and get its module + more_kwds = {} + try: + app_module = models.get_app(app) + more_kwds['__module__'] = app_module.__name__ + except ImproperlyConfigured: + # The app this belonged to has vanished, but thankfully we can still + # make a mock model, so ignore the error. + more_kwds['__module__'] = '_south_mock' + + more_kwds['Meta'] = meta + + # Make our model + fields.update(more_kwds) + + model = type( + str(name), + tuple(bases), + fields, + ) + + # If this is a stub model, change Objects to a whiny class + if stub: + model.objects = WhinyManager() + # Also, make sure they can't instantiate it + model.__init__ = whiny_method + else: + model.objects = NoDryRunManager(model.objects) + + if failed_fields: + model._failed_fields = failed_fields + + return model + + def retry_failed_fields(self): + "Tries to re-evaluate the _failed_fields for each model." + for modelkey, model in self.models.items(): + app, modelname = modelkey.split(".", 1) + if hasattr(model, "_failed_fields"): + for fname, (code, extra_imports) in model._failed_fields.items(): + try: + field = self.eval_in_context(code, app, extra_imports) + except (NameError, AttributeError, AssertionError, KeyError), e: + # It's failed again. Complain. + raise ValueError("Cannot successfully create field '%s' for model '%s': %s." % ( + fname, modelname, e + )) + else: + # Startup that field. + model.add_to_class(fname, field) + + +class WhinyManager(object): + "A fake manager that whines whenever you try to touch it. For stub models." + + def __getattr__(self, key): + raise AttributeError("You cannot use items from a stub model.") + + +class NoDryRunManager(object): + """ + A manager that always proxies through to the real manager, + unless a dry run is in progress. + """ + + def __init__(self, real): + self.real = real + + def __getattr__(self, name): + if db.dry_run: + raise AttributeError("You are in a dry run, and cannot access the ORM.\nWrap ORM sections in 'if not db.dry_run:', or if the whole migration is only a data migration, set no_dry_run = True on the Migration class.") + return getattr(self.real, name) + + +def whiny_method(*a, **kw): + raise ValueError("You cannot instantiate a stub model.") diff --git a/lib/python/south/orm.pyc b/lib/python/south/orm.pyc new file mode 100644 index 0000000000000000000000000000000000000000..81cb1488e814b7d974561509364d3bbcc9f927b6 GIT binary patch literal 11957 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 Zx`6*LIt!AA% literal 0 HcmV?d00001 diff --git a/lib/python/south/signals.py b/lib/python/south/signals.py new file mode 100644 index 0000000..f2938d5 --- /dev/null +++ b/lib/python/south/signals.py @@ -0,0 +1,24 @@ +""" +South-specific signals +""" + +from django.dispatch import Signal +from django.conf import settings + +# Sent at the start of the migration of an app +pre_migrate = Signal(providing_args=["app"]) + +# Sent after each successful migration of an app +post_migrate = Signal(providing_args=["app"]) + +# Sent after each run of a particular migration in a direction +ran_migration = Signal(providing_args=["app","migration","method"]) + +# Compatibility code for django.contrib.auth +# Is causing strange errors, removing for now (we might need to fix up orm first) +#if 'django.contrib.auth' in settings.INSTALLED_APPS: + #def create_permissions_compat(app, **kwargs): + #from django.db.models import get_app + #from django.contrib.auth.management import create_permissions + #create_permissions(get_app(app), (), 0) + #post_migrate.connect(create_permissions_compat) diff --git a/lib/python/south/signals.pyc b/lib/python/south/signals.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4e24f80f4beb4a1ce93414bd7bac75bb2915d67e GIT binary patch literal 483 zcmZ9Iu};G<5Qfi5o0dRn#l*;%rOn6yLOcONjab4Exj8pAN^C2>BA9t(9stg1MI~(c zxBt)QJKOR1EWNE>pKCa_G0%@Y{FVg)N>%|FK`Wq(po~BSA43~MH-d5mLPdZhz%eue zzCe^?_)n4(!31yuO*}+S0H;u7XQAFTOIp}H?UvrDT5olI<8@r?&1EfX9;2}?2H zgP(@8ib$Kr(!O8A<0T)aZ26OVZX1Svo~Ki%(g|Kogd#LR(iA&3TP) z(lmC3+B-@0Zp{ej{IpS9vt17t3efecIwV(g5xn*E2WAX=1.3 + from django.utils import unittest +except ImportError: + # earlier django... use unittest from stdlib + import unittest +# however, skipUnless was only added in Python 2.7; +# if not available, we need to do something else +try: + skipUnless = unittest.skipUnless #@UnusedVariable +except AttributeError: + def skipUnless(condition, message): + def decorator(testfunc): + @wraps(testfunc) + def wrapper(self): + if condition: + # Apply method + testfunc(self) + else: + # The skip exceptions are not available either... + print "Skipping", testfunc.__name__,"--", message + return wrapper + return decorator + +# Add the tests directory so fakeapp is on sys.path +test_root = os.path.dirname(__file__) +sys.path.append(test_root) + +# Note: the individual test files are imported below this. + +class Monkeypatcher(unittest.TestCase): + + """ + Base test class for tests that play with the INSTALLED_APPS setting at runtime. + """ + + def create_fake_app(self, name): + + class Fake: + pass + + fake = Fake() + fake.__name__ = name + try: + fake.migrations = __import__(name + ".migrations", {}, {}, ['migrations']) + except ImportError: + pass + return fake + + def setUp(self): + """ + Changes the Django environment so we can run tests against our test apps. + """ + if hasattr(self, 'installed_apps'): + hacks.store_app_cache_state() + hacks.set_installed_apps(self.installed_apps) + + def tearDown(self): + """ + Undoes what setUp did. + """ + if hasattr(self, 'installed_apps'): + hacks.reset_installed_apps() + hacks.restore_app_cache_state() + + +# Try importing all tests if asked for (then we can run 'em) +try: + skiptest = settings.SKIP_SOUTH_TESTS +except: + skiptest = True + +if not skiptest: + from south.tests.db import * + from south.tests.db_mysql import * + from south.tests.logic import * + from south.tests.autodetection import * + from south.tests.logger import * + from south.tests.inspector import * + from south.tests.freezer import * diff --git a/lib/python/south/tests/__init__.pyc b/lib/python/south/tests/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..64e29396a5d51b0fc1bba0350a1b66041ac0bd3e GIT binary patch literal 3271 zcmcImQFGHq5Z;q)Cw5{Ik^qI4jyen-mlmf_?L#}ALZHAD(o}YaVjfgQI@?NQ$<>{k zx`n4Og+J0?*dNe-yE0A!JO{xDShPY|7yxBd@JZvcYVI z6^3DSa6(~{CDv+VBL^}U&+C745rdbCUJ~^kj@;iw&mDT^P^ZN@+Y#@{y$YUnJmxwY zOY{neV4cHDVW98MR%7zHqG#%qf!9XFz~?aV%AsPNUO`dJs0`s&bPp5zME7C`UHsn% zew`NkyKVWLor7dN=7X|BH*X#T5CmB`(m_zpu&YL=eOYN2FdAuN!hseej6;8tMYaw> zlxJ~blRRr-K;|}@&0V7lv$`MkQ+W9eD>#HM2%mho|QX%W`l6=;=7L z(NGt2s^L817@5z|pomLi=}G3kzz|93NSRldU3jKSey7Ej%@Z^Y51-qi(H;lQi_*{- zub&q(G@c!X)*Gkc)H_V<5JT-f*zR<1J$!igPH<~yr{m2k&4XSs$!s#xZLukZ#oKW4 zZSi)$qCrZ>)tO62ppUY$(0lH3g*?sotQwN67$Lh@vy%#PP6nf{){$a4{K;^8%ISuR>ti!ULJPTNHUAvGo}Tfq`j2 zHkDBypd26UjLu|Nuvt{-(CUCU2vFy3Oc{2MoOQRZ>dIBuR8v)y&?jfDi)RauVKhLW z;5}r=5oquW4*$Nx_lx2lQUkYBMQ$O!1DJ|=wRwt0l8uviYZzt&%_*zBJNxM8p3Z(x ziaZziyTd`40CF#%l=<$#aZ{!*TeKW2RtVEn$9yWY-I6dcAq_(l zT3h(c23E=yG$LmYq7XG2V4c-=4Pw*?xbUPMpZTVwG>+8ShlJh4w!7nRxFd91Sy7F5 zz<&b@r{MqeE%-SQr+{aF=fLmcRsimYOck=F7bo#CigMKLGAo(;OTaeJEA-pISXP1H zU0A+PycV&o)nRcbKg=X6k0BFIk@E97(d8}+f;f**fE;)K3O85L9EVYQXH;H3vp*X& z>}0J{bL!3dqJV+wAiB}|7_H?z%ne8pic=$Zxm@abo*KDdg(zaFj5cohzR!zgQf}?G zydjm*S%ze$Ik5@WvsIr{*=Lay#w{d&vNy3>SV!0em(x(-iIakPN8T=ipr0_Gibs6O z1Vx@(%lPv9Ko*Nu7lJKZLvmQl#L@Zj!A{WmX}5bn=-%ygJEF!HB-@bfy2V6`E2aND zZ`$#m<=i?w6^y3lx7410Ly+c!B(j&^m<=a3kG0hi6LML<{8rO0?PJYA7iGbpm_~Vz zu>!O#Z*(Ul{X*+Mb@6_3LqPeqyt2NN(0_%~EURU;%3s%YHdNEWKe!j)b8Bj~a``%s MT-9)DAcW)Z|1N^nrT_o{ literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/autodetection.py b/lib/python/south/tests/autodetection.py new file mode 100644 index 0000000..dd66103 --- /dev/null +++ b/lib/python/south/tests/autodetection.py @@ -0,0 +1,353 @@ +from south.tests import unittest + +from south.creator.changes import AutoChanges, InitialChanges +from south.migration.base import Migrations +from south.tests import Monkeypatcher +from south.creator import freezer +from south.orm import FakeORM +from south.v2 import SchemaMigration + +class TestComparison(unittest.TestCase): + + """ + Tests the comparison methods of startmigration. + """ + + def test_no_change(self): + "Test with a completely unchanged definition." + + self.assertEqual( + AutoChanges.different_attributes( + ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['southdemo.Lizard']"}), + ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['southdemo.Lizard']"}), + ), + False, + ) + + self.assertEqual( + AutoChanges.different_attributes( + ('django.db.models.fields.related.ForeignKey', ['ohhai', 'there'], {'to': "somewhere", "from": "there"}), + ('django.db.models.fields.related.ForeignKey', ['ohhai', 'there'], {"from": "there", 'to': "somewhere"}), + ), + False, + ) + + + def test_pos_change(self): + "Test with a changed positional argument." + + self.assertEqual( + AutoChanges.different_attributes( + ('django.db.models.fields.CharField', ['hi'], {'to': "foo"}), + ('django.db.models.fields.CharField', [], {'to': "foo"}), + ), + True, + ) + + self.assertEqual( + AutoChanges.different_attributes( + ('django.db.models.fields.CharField', [], {'to': "foo"}), + ('django.db.models.fields.CharField', ['bye'], {'to': "foo"}), + ), + True, + ) + + self.assertEqual( + AutoChanges.different_attributes( + ('django.db.models.fields.CharField', ['pi'], {'to': "foo"}), + ('django.db.models.fields.CharField', ['pi'], {'to': "foo"}), + ), + False, + ) + + self.assertEqual( + AutoChanges.different_attributes( + ('django.db.models.fields.CharField', ['pisdadad'], {'to': "foo"}), + ('django.db.models.fields.CharField', ['pi'], {'to': "foo"}), + ), + True, + ) + + self.assertEqual( + AutoChanges.different_attributes( + ('django.db.models.fields.CharField', ['hi'], {}), + ('django.db.models.fields.CharField', [], {}), + ), + True, + ) + + self.assertEqual( + AutoChanges.different_attributes( + ('django.db.models.fields.CharField', [], {}), + ('django.db.models.fields.CharField', ['bye'], {}), + ), + True, + ) + + self.assertEqual( + AutoChanges.different_attributes( + ('django.db.models.fields.CharField', ['pi'], {}), + ('django.db.models.fields.CharField', ['pi'], {}), + ), + False, + ) + + self.assertEqual( + AutoChanges.different_attributes( + ('django.db.models.fields.CharField', ['pi'], {}), + ('django.db.models.fields.CharField', ['45fdfdf'], {}), + ), + True, + ) + + + def test_kwd_change(self): + "Test a changed keyword argument" + + self.assertEqual( + AutoChanges.different_attributes( + ('django.db.models.fields.CharField', ['pi'], {'to': "foo"}), + ('django.db.models.fields.CharField', ['pi'], {'to': "blue"}), + ), + True, + ) + + self.assertEqual( + AutoChanges.different_attributes( + ('django.db.models.fields.CharField', [], {'to': "foo"}), + ('django.db.models.fields.CharField', [], {'to': "blue"}), + ), + True, + ) + + self.assertEqual( + AutoChanges.different_attributes( + ('django.db.models.fields.CharField', ['b'], {'to': "foo"}), + ('django.db.models.fields.CharField', ['b'], {'to': "blue"}), + ), + True, + ) + + self.assertEqual( + AutoChanges.different_attributes( + ('django.db.models.fields.CharField', [], {'to': "foo"}), + ('django.db.models.fields.CharField', [], {}), + ), + True, + ) + + self.assertEqual( + AutoChanges.different_attributes( + ('django.db.models.fields.CharField', ['a'], {'to': "foo"}), + ('django.db.models.fields.CharField', ['a'], {}), + ), + True, + ) + + self.assertEqual( + AutoChanges.different_attributes( + ('django.db.models.fields.CharField', [], {}), + ('django.db.models.fields.CharField', [], {'to': "foo"}), + ), + True, + ) + + self.assertEqual( + AutoChanges.different_attributes( + ('django.db.models.fields.CharField', ['a'], {}), + ('django.db.models.fields.CharField', ['a'], {'to': "foo"}), + ), + True, + ) + + + + def test_backcompat_nochange(self): + "Test that the backwards-compatable comparison is working" + + self.assertEqual( + AutoChanges.different_attributes( + ('models.CharField', [], {}), + ('django.db.models.fields.CharField', [], {}), + ), + False, + ) + + self.assertEqual( + AutoChanges.different_attributes( + ('models.CharField', ['ack'], {}), + ('django.db.models.fields.CharField', ['ack'], {}), + ), + False, + ) + + self.assertEqual( + AutoChanges.different_attributes( + ('models.CharField', [], {'to':'b'}), + ('django.db.models.fields.CharField', [], {'to':'b'}), + ), + False, + ) + + self.assertEqual( + AutoChanges.different_attributes( + ('models.CharField', ['hah'], {'to':'you'}), + ('django.db.models.fields.CharField', ['hah'], {'to':'you'}), + ), + False, + ) + + self.assertEqual( + AutoChanges.different_attributes( + ('models.CharField', ['hah'], {'to':'you'}), + ('django.db.models.fields.CharField', ['hah'], {'to':'heh'}), + ), + True, + ) + + self.assertEqual( + AutoChanges.different_attributes( + ('models.CharField', ['hah'], {}), + ('django.db.models.fields.CharField', [], {'to':"orm['appname.hah']"}), + ), + False, + ) + + self.assertEqual( + AutoChanges.different_attributes( + ('models.CharField', ['hah'], {}), + ('django.db.models.fields.CharField', [], {'to':'hah'}), + ), + True, + ) + + self.assertEqual( + AutoChanges.different_attributes( + ('models.CharField', ['hah'], {}), + ('django.db.models.fields.CharField', [], {'to':'rrr'}), + ), + True, + ) + + self.assertEqual( + AutoChanges.different_attributes( + ('models.CharField', ['hah'], {}), + ('django.db.models.fields.IntField', [], {'to':'hah'}), + ), + True, + ) + +class TestNonManagedIgnored(Monkeypatcher): + + installed_apps = ["non_managed"] + + full_defs = { + 'non_managed.legacy': { + 'Meta': {'object_name': 'Legacy', 'db_table': "'legacy_table'", 'managed': 'False'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True'}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + } + } + + def test_not_added_init(self): + + migrations = Migrations("non_managed") + changes = InitialChanges(migrations) + change_list = changes.get_changes() + if list(change_list): + self.fail("Initial migration creates table for non-managed model") + + def test_not_added_auto(self): + + empty_defs = { } + class EmptyMigration(SchemaMigration): + "Serves as fake previous migration" + + def forwards(self, orm): + pass + + def backwards(self, orm): + pass + + models = empty_defs + + complete_apps = ['non_managed'] + + migrations = Migrations("non_managed") + empty_orm = FakeORM(EmptyMigration, "non_managed") + changes = AutoChanges( + migrations = migrations, + old_defs = empty_defs, + old_orm = empty_orm, + new_defs = self.full_defs, + ) + change_list = changes.get_changes() + if list(change_list): + self.fail("Auto migration creates table for non-managed model") + + def test_not_deleted_auto(self): + + empty_defs = { } + old_defs = freezer.freeze_apps(["non_managed"]) + class InitialMigration(SchemaMigration): + "Serves as fake previous migration" + + def forwards(self, orm): + pass + + def backwards(self, orm): + pass + + models = self.full_defs + + complete_apps = ['non_managed'] + + migrations = Migrations("non_managed") + initial_orm = FakeORM(InitialMigration, "non_managed") + changes = AutoChanges( + migrations = migrations, + old_defs = self.full_defs, + old_orm = initial_orm, + new_defs = empty_defs, + ) + change_list = changes.get_changes() + if list(change_list): + self.fail("Auto migration deletes table for non-managed model") + + def test_not_modified_auto(self): + + fake_defs = { + 'non_managed.legacy': { + 'Meta': {'object_name': 'Legacy', 'db_table': "'legacy_table'", 'managed': 'False'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True'}), + #'size': ('django.db.models.fields.IntegerField', [], {}) # The "change" is the addition of this field + } + } + class InitialMigration(SchemaMigration): + "Serves as fake previous migration" + + def forwards(self, orm): + pass + + def backwards(self, orm): + pass + + models = fake_defs + + complete_apps = ['non_managed'] + + from non_managed import models as dummy_import_to_force_loading_models # TODO: Does needing this indicate a bug in MokeyPatcher? + + migrations = Migrations("non_managed") + initial_orm = FakeORM(InitialMigration, "non_managed") + changes = AutoChanges( + migrations = migrations, + old_defs = fake_defs, + old_orm = initial_orm, + new_defs = self.full_defs + ) + change_list = changes.get_changes() + if list(change_list): + self.fail("Auto migration changes table for non-managed model") diff --git a/lib/python/south/tests/autodetection.pyc b/lib/python/south/tests/autodetection.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0759b886504d12c81b4fb68eb22c056c42aeb457 GIT binary patch literal 9138 zcmd5>OOqQ{6}~NLq|rP)jvwPVm>@%pl>}>&kT^V&V8@SOVh^q!vtUwGSC9Hi>Ty4Y zzAZcBvR6(4S+HfpA`4)_zu{M~hN4(f@O|gDS~Id{Y*RBSYpm=0zUQ9Dch0%0{%djW zeebi|T~&Ol`2P~V=C3FM{Bu;KRP3mAM>%X)R8&!MRjpT5TvO{c+gnx9jEd`Oy>8nz zS=CVM4cnek(X5J_YQ1UObrsF2cwVi~+jc`m3o2ez>x*d5D($GvBdX`9psBu4>akM4 zTwhYboYD>HoiBTjs$fB_wHHBc_+Nb54r=)L@UsOh6$nyjpaSh|uAKi5HeAqqEE z_}$uu@RJp}$qKhND?Usj0K2-v9892HaL7wi&$90*dJL&k)w)x6jyubhhSXZlcPdXR z%-?XENugI%-w~aGveZVRE_9~s8cTO#`Q$|C8+B-6$32%re1uB|04*rZMe2JJ%{z}~7nj|CL2;jQjlEvEX8OH$Od?BW<(lOhdlMk=TAQe7sBCw3 znp$C9Td5J-@}riY^@lO^w`0y?@?K^Jz|F4H;>3amA)o?>rm0J-k;cZhma(l+pnNMd zfsenufeG)v*9-8sDsbmgU&Vl2f-U3+8ndBxskLG-?n|uUdkDrNN77ipg2Gx}!$IaY zj}seNF0JLbbGopb!naE=wVrCqfrE$z?GYH5J#!UDu!5VQAHJg7oxmC%dA z{+_;eicRgZw5RVd#nekn*pp;V2XFc zq;KdsimHx*>eLaT$GH8@Yk-W8BMbK!m?JpQJ>nNe~bc4 zJ1l+_Z|?4EH+cRm3eO`4JuhdJ;(3tWFk+JvJTFMQp68O^?kg-_WkD}JjtQi8kWsWt zvLt3=QLoi2%@fV5%@6aFXi_m()8v+)_ctDPeOrjXV+3XBqV>`kA%kOR zA6FNdYo1VF*j)3ZRB+0&RA73iq|#v0DQdv%I!O~RwwKE5Xy4R*zq>8TPP(y)3`4|Z z7_gm>WXuyYJTF2FHf&22SVTA4i~jAF&i#FDkg2^`h3LKH)$d?a$Ff2E8Rsyc zyu!IxQRLX%QI8$<$YJXJ(AF#JVTEboSM2Sa$MNw64C5A7;8 zT6JINp6{V39z0rQ*5B%8+J_OhEFkvMtOfcm6s&Iv4B9xGJGqkZQr3N)7rxyHKcdj& zQtA0&BuTD>-Ij-=Ck7*8wE$s6M7Hj6_1GE@(h&3GAXY#sNs&|sfet*r1pEixhPJ6H z3y%F0DNt{jgz&cJ_b3n&d?|u}IPe_xTPeZh-`GbfD;wA$D88jzjvc84V6S?mtY756@m^9>JH&T|s!euPDbL?Pu?Mj%JV zB#LEdu)(1W9;avwsT$O9Dx}Ices-%~vQwKtEgpFa%J8?0n=HfJ(rhF-TQQ zkP=5xIFlN)0m2ZL0i*yrl9dC7EJ$s_y9*2vr!WT~g=SrCHUy*^l=DN>0aAY?sBI#Y zm;zFw7db=e!sqORRZoBI<~cL`ZJ~fG2|NxBt_~3i#IBiwzi=2OVDsjfG7C*qzCEGI z{e+{JS$vG5jql|1!aA{)FA;u3Xq*Z^Tzrsxlcb}Jfh-pSj{tO* z%w$qT06Mk2TLrMy1=!FJVEd=c z0kC~zSMJE3hu6sDSL*Ha2n*=I0(`DA%%a0 zu%D7jA|zszBK#Zw@h4EIJi&8(pn;+kd@H4Qb{3iFG78kB!;6QjvuvllP^4L> zP#1~%?L))~Otf>#_l(YlZ?x4AhKG_d?b^mV_hz}0PMEqRNs66$?;X)N>A3@nu5e3X hGVgSVJd+bYfF9uak_w>YDf(z-**QA;tDOG({{Zy}bU*+A literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/brokenapp/__init__.py b/lib/python/south/tests/brokenapp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/python/south/tests/brokenapp/__init__.pyc b/lib/python/south/tests/brokenapp/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aba893e422f365ebe775657d470c40e03abc17cb GIT binary patch literal 163 zcmZSn%*$mSA03d)00oRd+5w1*S%5?e14FO|NW@PANHCxg#XdkW{m|mnqGJ7|qQtzE z{5<{O%#ze%{gTp()RcVPoXjMB5H6@J$pFd~=a-gb=mX_TK+6+$R74 literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/brokenapp/migrations/0001_depends_on_unmigrated.py b/lib/python/south/tests/brokenapp/migrations/0001_depends_on_unmigrated.py new file mode 100644 index 0000000..d53f836 --- /dev/null +++ b/lib/python/south/tests/brokenapp/migrations/0001_depends_on_unmigrated.py @@ -0,0 +1,13 @@ +from south.db import db +from django.db import models + +class Migration: + + depends_on = [('unknown', '0001_initial')] + + def forwards(self): + pass + + def backwards(self): + pass + diff --git a/lib/python/south/tests/brokenapp/migrations/0001_depends_on_unmigrated.pyc b/lib/python/south/tests/brokenapp/migrations/0001_depends_on_unmigrated.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6d1bec311ff00c0a301231f2c0ad86b2b053a130 GIT binary patch literal 1044 zcmc&zJx{|h5WRe~&;k?(FtnBgDu1K`eSDI*iL z;>)|U<9m00Nj^{dBmVp_h0RFteT~zN&`5kDC;_SnCJ{vB$56&lB``_QPe67-rtksq z4)8QdL3UuA9pJb63r7|q@-ZD%w7847vQm}K_YmIcc`dxQ=7#`nzQO6v&?un1m|%_s zVoHvgFf%45kU~DrI=IhM#>Om-T^oOlcsLwhaBZ~LqMQ+2TGY}JcGM1t{$4^(?bsWAy3#^Smx2EgIvf5O zmEnF}Qcj(2ua3&(+=xnX?)$+(fi&gT-SsLb@#J*MuE2o)_mXldCMbpYbmLy!OEw}3 qTBL%43P-d literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/brokenapp/migrations/0002_depends_on_unknown.py b/lib/python/south/tests/brokenapp/migrations/0002_depends_on_unknown.py new file mode 100644 index 0000000..389af80 --- /dev/null +++ b/lib/python/south/tests/brokenapp/migrations/0002_depends_on_unknown.py @@ -0,0 +1,13 @@ +from south.db import db +from django.db import models + +class Migration: + + depends_on = [('fakeapp', '9999_unknown')] + + def forwards(self): + pass + + def backwards(self): + pass + diff --git a/lib/python/south/tests/brokenapp/migrations/0002_depends_on_unknown.pyc b/lib/python/south/tests/brokenapp/migrations/0002_depends_on_unknown.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7d4b9538e8fba360263f390fb318f8ac04579bf3 GIT binary patch literal 1025 zcmc&zJx{|h5WOUAp#>!V14|Z~ilstA2*C_OsTe3jl+&S<*=yl)_83?=Y*R1#GlO+E71_Nnx#3TPU@4oLJ!bm)`l zgXnqMA{X(3Bv`+)cPqE2nD)h_`)d{1c4}fA3`W<*ztyu z<-BpR-^`(+CnIYUWp%2AR?#H4(nNOsCWSsP^DJUnz1t=Bj9%Migni5Bbs-ffMOB-o ziAJN*MIz)%Dq#|>T9QNDKSZdxt~Y5d3}eH!An)(|W3I)1xN|eJbXGX>M=-YJL(U-% zSaETiZgFx+qS8txi5)ltGqEmPb61|2=OyOu?$UDTANvw>3T`r(OwaH8{a`yZZuU4E n*Y=^vVqkKUEnZW#(Cp$NbKlrs3U0Knni^j-ANLEKdZ+#mgDdr~ literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/brokenapp/migrations/0003_depends_on_higher.py b/lib/python/south/tests/brokenapp/migrations/0003_depends_on_higher.py new file mode 100644 index 0000000..319069b --- /dev/null +++ b/lib/python/south/tests/brokenapp/migrations/0003_depends_on_higher.py @@ -0,0 +1,13 @@ +from south.db import db +from django.db import models + +class Migration: + + depends_on = [('brokenapp', '0004_higher')] + + def forwards(self): + pass + + def backwards(self): + pass + diff --git a/lib/python/south/tests/brokenapp/migrations/0003_depends_on_higher.pyc b/lib/python/south/tests/brokenapp/migrations/0003_depends_on_higher.pyc new file mode 100644 index 0000000000000000000000000000000000000000..098a7c9cc37d110510a0c78216be3cdd046ebffd GIT binary patch literal 1022 zcmc&yJx{|h5WRe~&?2P%2bL_eS0DU2QyfDu2&Cp%i#T zhDzHti*ge}r?nEojr{^8v8{H?-{NqR3nw}KX64cUb%j#~UKYe<7>>eG)GNj-kCO2{ kpUR9Q?zAYMQ=J=fk;42Nd&|J<>Y}UgCDHNnp9GWe8>>I_eE zEUI1G@y{3A{PSe8kWcqZ=nu#L6_Q&Z)A%u{0BQyq14eoTRRlGMjHAv$k3bLL1K=It zF&ls$LK+`oZ25&JW+?fHQstdILb9afa+pdERaP6 zh6D~t8dK6GIq&I)f(FRYJq^X~V$`6kOBc>mtIkArmmw1m&a_VCt!lJwL|S^|gx|dy zZO^JQ7ew1PU*MgyJ6{OA_vCLkrcv9isP_+WVm_ar%c5Ktrk!ETF{WSH_C>YY(W@OH z2KIN%3C-egcxfEDT5bYhQhLBKwJy9y%0|^jNx~Ckb?@e7VM(vr6}s837QtM11dd~4M0%bl|+Z)s@#=h zon83c$3OI6=nqK087i`xyD9E)=b_zsd^;QbyT1BO^4rUdrcZ$H_ZapPMB(+QAR2lU zd*m_iQ{mGvpm+g%Kz|U$i=-AvdvxBQo=0ktg;zws#!I9cbBd*zVwu$PoML6BSRu7a zoz^{Up1;G>@-TA^kfBk!urUaLvg-AZXNRxw?}=nhBB2cjKqoXr-gZPHD#IUuz|>H(?U8E_wp=|@mg{4rN@ zpEKA7ISB=B6V5`WZVjD2h*C_{DA+t1Tw{^eTezZ^-Qx!`1_S&vW3?pgj*rwX9QYi=vL_9%;RU{QsAvOx zvy7O zTZuFh3t+fgm~@J9|JLeN1!S?gg;>m=nM&vV@@GAf&fvJT=BWN0%RbygPl`OPaWDoa zD`e;ulIZ89(|tX4g^`^VskKS!Tonlfct38L(GDhaz)}$>PWE`EFJ0{=LwkrOn-_Vh z@3?N9ac+P1QxMe6-wAd+{DMtyGuR87;F`gj*YtOnBMA&<@^Dy!e?Dn7{(%9VP7kWOQ(PvON&RbS6n7dW)!*VoR-j=_za1b1{{sUzw4%h$y literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/circular_a/__init__.py b/lib/python/south/tests/circular_a/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/python/south/tests/circular_a/__init__.pyc b/lib/python/south/tests/circular_a/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..90a6e5af657176b6926a2a14a1b65872c3127735 GIT binary patch literal 164 zcmZSn%*$mSA03d)00oRd+5w1*S%5?e14FO|NW@PANHCxg#lAo>{m|mnqGJ7|qQtzE z{5<{O%#ze%{gTp()RcVPoXjMB5H6@J$pFd~=a-gb=mX_TiuIE-i;_!o5{u#!_2c6+ d^D;}~@5aj1^{yxCsY6c literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/circular_a/migrations/0001_first.py b/lib/python/south/tests/circular_a/migrations/0001_first.py new file mode 100644 index 0000000..b0d90eb --- /dev/null +++ b/lib/python/south/tests/circular_a/migrations/0001_first.py @@ -0,0 +1,13 @@ +from south.db import db +from django.db import models + +class Migration: + + depends_on = [('circular_b', '0001_first')] + + def forwards(self): + pass + + def backwards(self): + pass + diff --git a/lib/python/south/tests/circular_a/migrations/0001_first.pyc b/lib/python/south/tests/circular_a/migrations/0001_first.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3426d29d92a229cdb1e46d86a10b2151798d955b GIT binary patch literal 978 zcmcgqJ5R$f5WYNGXc1EX14|a_GIc=+!3;vF7$}3~#BLi%;wt9|nBgDu1K`f-V`ZUM z?(*IFao;_n@6+K_K0oBJSrPrONpMO^(-%MqPz5j#AYeO$GK4CEc|>*ux(_;rFMv;g zr+Eyz2eage?iN3^l7Ns4`KheKeZrO1rbIU&cvmbN5qsJR) z%0=g5Kh8;sM>GwJtWmYLwU`yopm6QG(e}J7GQqO`a4XtF+q#vYeTQPlNQr8jD2hF? zG2J<)=NGnlQ;kN($OVAl@0?REz~T6E*D{sAdcr4UYzQaJJB?Uz5xcfYMGE(}c zNJ-6a%dM%++SD4Qty3P1lm};@ibwxk|CDL_t}}stI0y&PW+41vaR$DnW1Xdh!H=ze dQT5WYiwDTRaah`ZY%aT6UNHwh(xYG${sKip;bQ;* literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/circular_a/migrations/__init__.py b/lib/python/south/tests/circular_a/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/python/south/tests/circular_a/migrations/__init__.pyc b/lib/python/south/tests/circular_a/migrations/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..11ac6fa1e77c737d50594fc20b988ddcabac7983 GIT binary patch literal 175 zcmZ9GK?(vf3`Hxt5W#!QMzeGy;t2*}5Cj*Y(-tStX{AX)&+P?F!Igpc1OJmxmY?nB zfcJ|tzgg%$E4)(i#tdC^`cB3C3E$L%D~G}`7MpxdFpvZWsjuMkF472%=UXPtI9HiK mb762)T#dG%g_$133gyIrNQne-uNP}AKhD|L7*6d4R?HWB#VgAI literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/circular_a/models.py b/lib/python/south/tests/circular_a/models.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/python/south/tests/circular_a/models.pyc b/lib/python/south/tests/circular_a/models.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7e44895c7a596f2d52a486cbf0368638d68bd6e5 GIT binary patch literal 162 zcmZSn%*$mSA03d)00oRd+5w1*S%5?e14FO|NW@PANHCxg#ojg!6 aQge#+fQmSP=Gg$br8%i~AX|%pm;nGCz$T^u literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/circular_b/__init__.py b/lib/python/south/tests/circular_b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/python/south/tests/circular_b/__init__.pyc b/lib/python/south/tests/circular_b/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b81dbf393d3b917e8f1abc8d9e5ef2162bdade68 GIT binary patch literal 164 zcmZSn%*$mSA03d)00oRd+5w1*S%5?e14FO|NW@PANHCxg#lAo>{m|mnqGJ7|qQtzE z{5<{O%#ze%{gTp()RcVPoXjMB5H6@J$pFd~=a-gb=mX_TiuIE-i;_!o5{u%K^yA|* d^D;}~@5aj1^{z9CshCd literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/circular_b/migrations/0001_first.py b/lib/python/south/tests/circular_b/migrations/0001_first.py new file mode 100644 index 0000000..b11b120 --- /dev/null +++ b/lib/python/south/tests/circular_b/migrations/0001_first.py @@ -0,0 +1,13 @@ +from south.db import db +from django.db import models + +class Migration: + + depends_on = [('circular_a', '0001_first')] + + def forwards(self): + pass + + def backwards(self): + pass + diff --git a/lib/python/south/tests/circular_b/migrations/0001_first.pyc b/lib/python/south/tests/circular_b/migrations/0001_first.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2cb260217a02539fb6124bce85b09a4f563931d6 GIT binary patch literal 978 zcmcgqJ5R$f5WYNGXc1EX14|a_GIc=+!3;vF7$}3~#BLi%;wt9|nBgDu1K`f-V`ZUM z?(*IFao;_n@6+K_K0oBJSrPrONpMO^(-%MqPz5j#AYeO$GK4CEc|>*ux(_;rFMv;g zr+Eyz2eage?iN3^l7Ns4`KheKeZrO1rbIU&cvmbN5lE{XYMGE(}c zNJ-6a%dM%++SD4Qty3P1lm};@ibwxk|CDL_t}}stI0y&PW+41vaR$DnW1Xdh!H=ze dQT5WYiwDTRaah`ZY%aT6UNHwh(xYG${sKpe;bs5; literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/circular_b/migrations/__init__.py b/lib/python/south/tests/circular_b/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/python/south/tests/circular_b/migrations/__init__.pyc b/lib/python/south/tests/circular_b/migrations/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..32492c1abc5d32d43f62ea1d00823fa973a4bd92 GIT binary patch literal 175 zcmZ9GO$q`r423JY5W#!QMzeGy;t2*}5Cj*Ye>j1eR+<#_++M>p9RY;63ul^0VC> z@P6^;Hw)cog;y%Un4xP<->H~C;hTDJ)0p_~{HDUl%V^= 0)' is stripped + db.alter_column('test_multiword', 'col_integer', models.PositiveIntegerField(null=True)) + db.alter_column('test_multiword', 'col_smallint', models.PositiveSmallIntegerField(null=True)) + + # test if 'with timezone' is preserved + if db.backend_name == "postgres": + db.execute("INSERT INTO test_multiword (col_datetime) VALUES ('2009-04-24 14:20:55+02')") + db.alter_column('test_multiword', 'col_datetime', models.DateTimeField(auto_now=True)) + assert db.execute("SELECT col_datetime = '2009-04-24 14:20:55+02' FROM test_multiword")[0][0] + + db.delete_table("test_multiword") + + def test_alter_constraints(self): + """ + Tests that going from a PostiveIntegerField to an IntegerField drops + the constraint on the database. + """ + # Only applies to databases that support CHECK constraints + if not db.has_check_constraints: + return + # Make the test table + db.create_table("test_alterc", [ + ('num', models.PositiveIntegerField()), + ]) + db.execute_deferred_sql() + # Add in some test values + db.execute("INSERT INTO test_alterc (num) VALUES (1)") + db.execute("INSERT INTO test_alterc (num) VALUES (2)") + # Ensure that adding a negative number is bad + db.commit_transaction() + db.start_transaction() + try: + db.execute("INSERT INTO test_alterc (num) VALUES (-3)") + except: + db.rollback_transaction() + else: + self.fail("Could insert a negative integer into a PositiveIntegerField.") + # Alter it to a normal IntegerField + db.alter_column("test_alterc", "num", models.IntegerField()) + db.execute_deferred_sql() + # It should now work + db.execute("INSERT INTO test_alterc (num) VALUES (-3)") + db.delete_table("test_alterc") + # We need to match up for tearDown + db.start_transaction() + + def test_unique(self): + """ + Tests creating/deleting unique constraints. + """ + + # SQLite backend doesn't support this yet. + if db.backend_name == "sqlite3": + return + + db.create_table("test_unique2", [ + ('id', models.AutoField(primary_key=True)), + ]) + db.create_table("test_unique", [ + ('spam', models.BooleanField(default=False)), + ('eggs', models.IntegerField()), + ('ham', models.ForeignKey(db.mock_model('Unique2', 'test_unique2'))), + ]) + db.execute_deferred_sql() + # Add a constraint + db.create_unique("test_unique", ["spam"]) + db.execute_deferred_sql() + # Shouldn't do anything during dry-run + db.dry_run = True + db.delete_unique("test_unique", ["spam"]) + db.dry_run = False + db.delete_unique("test_unique", ["spam"]) + db.create_unique("test_unique", ["spam"]) + # Special preparations for Sql Server + if db.backend_name == "pyodbc": + db.execute("SET IDENTITY_INSERT test_unique2 ON;") + db.execute("INSERT INTO test_unique2 (id) VALUES (1)") + db.execute("INSERT INTO test_unique2 (id) VALUES (2)") + db.commit_transaction() + db.start_transaction() + + + # Test it works + TRUE = (True,) + FALSE = (False,) + db.execute("INSERT INTO test_unique (spam, eggs, ham_id) VALUES (%s, 0, 1)", TRUE) + db.execute("INSERT INTO test_unique (spam, eggs, ham_id) VALUES (%s, 1, 2)", FALSE) + try: + db.execute("INSERT INTO test_unique (spam, eggs, ham_id) VALUES (%s, 2, 1)", FALSE) + except: + db.rollback_transaction() + else: + self.fail("Could insert non-unique item.") + + # Drop that, add one only on eggs + db.delete_unique("test_unique", ["spam"]) + db.execute("DELETE FROM test_unique") + db.create_unique("test_unique", ["eggs"]) + db.start_transaction() + + # Test similarly + db.execute("INSERT INTO test_unique (spam, eggs, ham_id) VALUES (%s, 0, 1)", TRUE) + db.execute("INSERT INTO test_unique (spam, eggs, ham_id) VALUES (%s, 1, 2)", FALSE) + try: + db.execute("INSERT INTO test_unique (spam, eggs, ham_id) VALUES (%s, 1, 1)", TRUE) + except: + db.rollback_transaction() + else: + self.fail("Could insert non-unique item.") + + # Drop those, test combined constraints + db.delete_unique("test_unique", ["eggs"]) + db.execute("DELETE FROM test_unique") + db.create_unique("test_unique", ["spam", "eggs", "ham_id"]) + db.start_transaction() + # Test similarly + db.execute("INSERT INTO test_unique (spam, eggs, ham_id) VALUES (%s, 0, 1)", TRUE) + db.execute("INSERT INTO test_unique (spam, eggs, ham_id) VALUES (%s, 1, 1)", FALSE) + try: + db.execute("INSERT INTO test_unique (spam, eggs, ham_id) VALUES (%s, 0, 1)", TRUE) + except: + db.rollback_transaction() + else: + self.fail("Could insert non-unique pair.") + db.delete_unique("test_unique", ["spam", "eggs", "ham_id"]) + db.start_transaction() + + def test_alter_unique(self): + """ + Tests that unique constraints are not affected when + altering columns (that's handled by create_/delete_unique) + """ + db.create_table("test_alter_unique", [ + ('spam', models.IntegerField()), + ('eggs', models.IntegerField(unique=True)), + ]) + db.execute_deferred_sql() + + # Make sure the unique constraint is created + db.execute('INSERT INTO test_alter_unique (spam, eggs) VALUES (0, 42)') + db.commit_transaction() + db.start_transaction() + try: + db.execute("INSERT INTO test_alter_unique (spam, eggs) VALUES (1, 42)") + except: + pass + else: + self.fail("Could insert the same integer twice into a unique field.") + db.rollback_transaction() + + # Alter without unique=True (should not affect anything) + db.alter_column("test_alter_unique", "eggs", models.IntegerField()) + + # Insertion should still fail + db.start_transaction() + try: + db.execute("INSERT INTO test_alter_unique (spam, eggs) VALUES (1, 42)") + except: + pass + else: + self.fail("Could insert the same integer twice into a unique field after alter_column with unique=False.") + db.rollback_transaction() + + # Delete the unique index/constraint + if db.backend_name != "sqlite3": + db.delete_unique("test_alter_unique", ["eggs"]) + db.delete_table("test_alter_unique") + db.start_transaction() + + def test_capitalised_constraints(self): + """ + Under PostgreSQL at least, capitalised constraints must be quoted. + """ + db.create_table("test_capconst", [ + ('SOMECOL', models.PositiveIntegerField(primary_key=True)), + ]) + # Alter it so it's not got the check constraint + db.alter_column("test_capconst", "SOMECOL", models.IntegerField()) + + def test_text_default(self): + """ + MySQL cannot have blank defaults on TEXT columns. + """ + db.create_table("test_textdef", [ + ('textcol', models.TextField(blank=True)), + ]) + + def test_text_to_char(self): + """ + On Oracle, you can't simply ALTER TABLE MODIFY a textfield to a charfield + """ + value = "kawabanga" + db.create_table("test_text_to_char", [ + ('textcol', models.TextField()), + ]) + db.execute_deferred_sql() + db.execute("INSERT INTO test_text_to_char VALUES (%s)", [value]) + db.alter_column("test_text_to_char", "textcol", models.CharField(max_length=100)) + db.execute_deferred_sql() + after = db.execute("select * from test_text_to_char")[0][0] + self.assertEqual(value, after, "Change from text to char altered value [ %s != %s ]" % (`value`,`after`)) + + def test_char_to_text(self): + """ + On Oracle, you can't simply ALTER TABLE MODIFY a charfield to a textfield either + """ + value = "agnabawak" + db.create_table("test_char_to_text", [ + ('textcol', models.CharField(max_length=100)), + ]) + db.execute_deferred_sql() + db.execute("INSERT INTO test_char_to_text VALUES (%s)", [value]) + db.alter_column("test_char_to_text", "textcol", models.TextField()) + db.execute_deferred_sql() + after = db.execute("select * from test_char_to_text")[0][0] + after = unicode(after) # Oracle text fields return a sort of lazy string -- force evaluation + self.assertEqual(value, after, "Change from char to text altered value [ %s != %s ]" % (`value`,`after`)) + + def test_datetime_default(self): + """ + Test that defaults are created correctly for datetime columns + """ + end_of_world = datetime.datetime(2012, 12, 21, 0, 0, 1) + + try: + from django.utils import timezone + except ImportError: + pass + else: + from django.conf import settings + if getattr(settings, 'USE_TZ', False): + end_of_world = end_of_world.replace(tzinfo=timezone.utc) + + db.create_table("test_datetime_def", [ + ('col0', models.IntegerField(null=True)), + ('col1', models.DateTimeField(default=end_of_world)), + ('col2', models.DateTimeField(null=True)), + ]) + db.execute_deferred_sql() + db.alter_column("test_datetime_def", "col2", models.DateTimeField(default=end_of_world)) + db.add_column("test_datetime_def", "col3", models.DateTimeField(default=end_of_world)) + db.execute_deferred_sql() + # There should not be a default in the database for col1 + db.commit_transaction() + db.start_transaction() + self.assertRaises( + IntegrityError, + db.execute, "insert into test_datetime_def (col0) values (null)" + ) + db.rollback_transaction() + db.start_transaction() + # There should be for the others + db.execute("insert into test_datetime_def (col0, col1) values (null, %s)", [end_of_world]) + ends = db.execute("select col1,col2,col3 from test_datetime_def")[0] + self.failUnlessEqual(len(ends), 3) + for e in ends: + self.failUnlessEqual(e, end_of_world) + + def test_add_unique_fk(self): + """ + Test adding a ForeignKey with unique=True or a OneToOneField + """ + db.create_table("test_add_unique_fk", [ + ('spam', models.BooleanField(default=False)) + ]) + + db.add_column("test_add_unique_fk", "mock1", models.ForeignKey(db.mock_model('Mock', 'mock'), null=True, unique=True)) + db.add_column("test_add_unique_fk", "mock2", models.OneToOneField(db.mock_model('Mock', 'mock'), null=True)) + + db.delete_table("test_add_unique_fk") + + def test_column_constraint(self): + """ + Tests that the value constraint of PositiveIntegerField is enforced on + the database level. + """ + if not db.has_check_constraints: + return + + db.create_table("test_column_constraint", [ + ('spam', models.PositiveIntegerField()), + ]) + db.execute_deferred_sql() + + # Make sure we can't insert negative values + db.commit_transaction() + db.start_transaction() + try: + db.execute("INSERT INTO test_column_constraint VALUES (-42)") + except: + pass + else: + self.fail("Could insert a negative value into a PositiveIntegerField.") + db.rollback_transaction() + + # remove constraint + db.alter_column("test_column_constraint", "spam", models.IntegerField()) + db.execute_deferred_sql() + # make sure the insertion works now + db.execute('INSERT INTO test_column_constraint VALUES (-42)') + db.execute('DELETE FROM test_column_constraint') + + # add it back again + db.alter_column("test_column_constraint", "spam", models.PositiveIntegerField()) + db.execute_deferred_sql() + # it should fail again + db.start_transaction() + try: + db.execute("INSERT INTO test_column_constraint VALUES (-42)") + except: + pass + else: + self.fail("Could insert a negative value after changing an IntegerField to a PositiveIntegerField.") + db.rollback_transaction() + + db.delete_table("test_column_constraint") + db.start_transaction() + + def test_sql_defaults(self): + """ + Test that sql default value is correct for non-string field types. + Datetimes are handled in test_datetime_default. + """ + + class CustomField(models.CharField): + __metaclass__ = models.SubfieldBase + description = 'CustomField' + def get_default(self): + if self.has_default(): + if callable(self.default): + return self.default() + return self.default + return super(CustomField, self).get_default() + def get_prep_value(self, value): + if not value: + return value + return ','.join(map(str, value)) + def to_python(self, value): + if not value or isinstance(value, list): + return value + return map(int, value.split(',')) + + false_value = db.has_booleans and 'False' or '0' + defaults = ( + (models.CharField(default='sukasuka'), 'DEFAULT \'sukasuka'), + (models.BooleanField(default=False), 'DEFAULT %s' % false_value), + (models.IntegerField(default=42), 'DEFAULT 42'), + (CustomField(default=[2012, 2018, 2021, 2036]), 'DEFAULT \'2012,2018,2021,2036') + ) + for field, sql_test_str in defaults: + sql = db.column_sql('fish', 'YAAAAAAZ', field) + if sql_test_str not in sql: + self.fail("default sql value was not properly generated for field %r.\nSql was %s" % (field, sql)) + + def test_make_added_foreign_key_not_null(self): + # Table for FK to target + User = db.mock_model(model_name='User', db_table='auth_user', db_tablespace='', pk_field_name='id', pk_field_type=models.AutoField, pk_field_args=[], pk_field_kwargs={}) + # Table with no foreign key + db.create_table("test_fk", [ + ('eggs', models.IntegerField()), + ]) + db.execute_deferred_sql() + + # Add foreign key + db.add_column("test_fk", 'foreik', models.ForeignKey(User, null=True), + keep_default = False) + db.execute_deferred_sql() + + # Make the FK null + db.alter_column("test_fk", "foreik_id", models.ForeignKey(User)) + db.execute_deferred_sql() + +class TestCacheGeneric(unittest.TestCase): + base_ops_cls = generic.DatabaseOperations + def setUp(self): + class CacheOps(self.base_ops_cls): + def __init__(self): + self._constraint_cache = {} + self.cache_filled = 0 + self.settings = {'NAME': 'db'} + + def _fill_constraint_cache(self, db, table): + self.cache_filled += 1 + self._constraint_cache.setdefault(db, {}) + self._constraint_cache[db].setdefault(table, {}) + + @generic.invalidate_table_constraints + def clear_con(self, table): + pass + + @generic.copy_column_constraints + def cp_column(self, table, column_old, column_new): + pass + + @generic.delete_column_constraints + def rm_column(self, table, column): + pass + + @generic.copy_column_constraints + @generic.delete_column_constraints + def mv_column(self, table, column_old, column_new): + pass + + def _get_setting(self, attr): + return self.settings[attr] + self.CacheOps = CacheOps + + def test_cache(self): + ops = self.CacheOps() + self.assertEqual(0, ops.cache_filled) + self.assertFalse(ops.lookup_constraint('db', 'table')) + self.assertEqual(1, ops.cache_filled) + self.assertFalse(ops.lookup_constraint('db', 'table')) + self.assertEqual(1, ops.cache_filled) + ops.clear_con('table') + self.assertEqual(1, ops.cache_filled) + self.assertFalse(ops.lookup_constraint('db', 'table')) + self.assertEqual(2, ops.cache_filled) + self.assertFalse(ops.lookup_constraint('db', 'table', 'column')) + self.assertEqual(2, ops.cache_filled) + + cache = ops._constraint_cache + cache['db']['table']['column'] = 'constraint' + self.assertEqual('constraint', ops.lookup_constraint('db', 'table', 'column')) + self.assertEqual([('column', 'constraint')], ops.lookup_constraint('db', 'table')) + self.assertEqual(2, ops.cache_filled) + + # invalidate_table_constraints + ops.clear_con('new_table') + self.assertEqual('constraint', ops.lookup_constraint('db', 'table', 'column')) + self.assertEqual(2, ops.cache_filled) + + self.assertFalse(ops.lookup_constraint('db', 'new_table')) + self.assertEqual(3, ops.cache_filled) + + # delete_column_constraints + cache['db']['table']['column'] = 'constraint' + self.assertEqual('constraint', ops.lookup_constraint('db', 'table', 'column')) + ops.rm_column('table', 'column') + self.assertEqual([], ops.lookup_constraint('db', 'table', 'column')) + self.assertEqual([], ops.lookup_constraint('db', 'table', 'noexist_column')) + + # copy_column_constraints + cache['db']['table']['column'] = 'constraint' + self.assertEqual('constraint', ops.lookup_constraint('db', 'table', 'column')) + ops.cp_column('table', 'column', 'column_new') + self.assertEqual('constraint', ops.lookup_constraint('db', 'table', 'column_new')) + self.assertEqual('constraint', ops.lookup_constraint('db', 'table', 'column')) + + # copy + delete + cache['db']['table']['column'] = 'constraint' + self.assertEqual('constraint', ops.lookup_constraint('db', 'table', 'column')) + ops.mv_column('table', 'column', 'column_new') + self.assertEqual('constraint', ops.lookup_constraint('db', 'table', 'column_new')) + self.assertEqual([], ops.lookup_constraint('db', 'table', 'column')) + return + + def test_valid(self): + ops = self.CacheOps() + # none of these should vivify a table into a valid state + self.assertFalse(ops._is_valid_cache('db', 'table')) + self.assertFalse(ops._is_valid_cache('db', 'table')) + ops.clear_con('table') + self.assertFalse(ops._is_valid_cache('db', 'table')) + ops.rm_column('table', 'column') + self.assertFalse(ops._is_valid_cache('db', 'table')) + + # these should change the cache state + ops.lookup_constraint('db', 'table') + self.assertTrue(ops._is_valid_cache('db', 'table')) + ops.lookup_constraint('db', 'table', 'column') + self.assertTrue(ops._is_valid_cache('db', 'table')) + ops.clear_con('table') + self.assertFalse(ops._is_valid_cache('db', 'table')) + + def test_valid_implementation(self): + # generic fills the cache on a per-table basis + ops = self.CacheOps() + self.assertFalse(ops._is_valid_cache('db', 'table')) + self.assertFalse(ops._is_valid_cache('db', 'other_table')) + ops.lookup_constraint('db', 'table') + self.assertTrue(ops._is_valid_cache('db', 'table')) + self.assertFalse(ops._is_valid_cache('db', 'other_table')) + ops.lookup_constraint('db', 'other_table') + self.assertTrue(ops._is_valid_cache('db', 'table')) + self.assertTrue(ops._is_valid_cache('db', 'other_table')) + ops.clear_con('table') + self.assertFalse(ops._is_valid_cache('db', 'table')) + self.assertTrue(ops._is_valid_cache('db', 'other_table')) + +if mysql: + class TestCacheMysql(TestCacheGeneric): + base_ops_cls = mysql.DatabaseOperations + + def test_valid_implementation(self): + # mysql fills the cache on a per-db basis + ops = self.CacheOps() + self.assertFalse(ops._is_valid_cache('db', 'table')) + self.assertFalse(ops._is_valid_cache('db', 'other_table')) + ops.lookup_constraint('db', 'table') + self.assertTrue(ops._is_valid_cache('db', 'table')) + self.assertTrue(ops._is_valid_cache('db', 'other_table')) + ops.lookup_constraint('db', 'other_table') + self.assertTrue(ops._is_valid_cache('db', 'table')) + self.assertTrue(ops._is_valid_cache('db', 'other_table')) + ops.clear_con('table') + self.assertFalse(ops._is_valid_cache('db', 'table')) + self.assertTrue(ops._is_valid_cache('db', 'other_table')) diff --git a/lib/python/south/tests/db.pyc b/lib/python/south/tests/db.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f1a591647535d62d0c08eb7fc837138fd7962383 GIT binary patch literal 29603 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 AYXATM literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/db_mysql.py b/lib/python/south/tests/db_mysql.py new file mode 100644 index 0000000..b42bfee --- /dev/null +++ b/lib/python/south/tests/db_mysql.py @@ -0,0 +1,165 @@ +# Additional MySQL-specific tests +# Written by: F. Gabriel Gosselin +# Based on tests by: aarranz +from south.tests import unittest + + +from south.db import db, generic, mysql +from django.db import connection, models + + +class TestMySQLOperations(unittest.TestCase): + """MySQL-specific tests""" + def setUp(self): + db.debug = False + db.clear_deferred_sql() + + def tearDown(self): + pass + + def _create_foreign_tables(self, main_name, reference_name): + # Create foreign table and model + Foreign = db.mock_model(model_name='Foreign', db_table=reference_name, + db_tablespace='', pk_field_name='id', + pk_field_type=models.AutoField, + pk_field_args=[]) + db.create_table(reference_name, [ + ('id', models.AutoField(primary_key=True)), + ]) + # Create table with foreign key + db.create_table(main_name, [ + ('id', models.AutoField(primary_key=True)), + ('foreign', models.ForeignKey(Foreign)), + ]) + return Foreign + + def test_constraint_references(self): + """Tests that referred table is reported accurately""" + if db.backend_name != "mysql": + return + main_table = 'test_cns_ref' + reference_table = 'test_cr_foreign' + db.start_transaction() + self._create_foreign_tables(main_table, reference_table) + db.execute_deferred_sql() + constraint = db._find_foreign_constraints(main_table, 'foreign_id')[0] + constraint_name = 'foreign_id_refs_id_%x' % (abs(hash((main_table, + reference_table)))) + self.assertEquals(constraint_name, constraint) + references = db._lookup_constraint_references(main_table, constraint) + self.assertEquals((reference_table, 'id'), references) + db.delete_table(main_table) + db.delete_table(reference_table) + + def test_reverse_column_constraint(self): + """Tests that referred column in a foreign key (ex. id) is found""" + if db.backend_name != "mysql": + return + main_table = 'test_reverse_ref' + reference_table = 'test_rr_foreign' + db.start_transaction() + self._create_foreign_tables(main_table, reference_table) + db.execute_deferred_sql() + inverse = db._lookup_reverse_constraint(reference_table, 'id') + (cname, rev_table, rev_column) = inverse[0] + self.assertEquals(main_table, rev_table) + self.assertEquals('foreign_id', rev_column) + db.delete_table(main_table) + db.delete_table(reference_table) + + def test_delete_fk_column(self): + if db.backend_name != "mysql": + return + main_table = 'test_drop_foreign' + ref_table = 'test_df_ref' + self._create_foreign_tables(main_table, ref_table) + db.execute_deferred_sql() + constraints = db._find_foreign_constraints(main_table, 'foreign_id') + self.assertEquals(len(constraints), 1) + db.delete_column(main_table, 'foreign_id') + constraints = db._find_foreign_constraints(main_table, 'foreign_id') + self.assertEquals(len(constraints), 0) + db.delete_table(main_table) + db.delete_table(ref_table) + + def test_rename_fk_column(self): + if db.backend_name != "mysql": + return + main_table = 'test_rename_foreign' + ref_table = 'test_rf_ref' + self._create_foreign_tables(main_table, ref_table) + db.execute_deferred_sql() + constraints = db._find_foreign_constraints(main_table, 'foreign_id') + self.assertEquals(len(constraints), 1) + db.rename_column(main_table, 'foreign_id', 'reference_id') + db.execute_deferred_sql() #Create constraints + constraints = db._find_foreign_constraints(main_table, 'reference_id') + self.assertEquals(len(constraints), 1) + db.delete_table(main_table) + db.delete_table(ref_table) + + def test_rename_fk_inbound(self): + """ + Tests that the column referred to by an external column can be renamed. + Edge case, but also useful as stepping stone to renaming tables. + """ + if db.backend_name != "mysql": + return + main_table = 'test_rename_fk_inbound' + ref_table = 'test_rfi_ref' + self._create_foreign_tables(main_table, ref_table) + db.execute_deferred_sql() + constraints = db._lookup_reverse_constraint(ref_table, 'id') + self.assertEquals(len(constraints), 1) + db.rename_column(ref_table, 'id', 'rfi_id') + db.execute_deferred_sql() #Create constraints + constraints = db._lookup_reverse_constraint(ref_table, 'rfi_id') + self.assertEquals(len(constraints), 1) + cname = db._find_foreign_constraints(main_table, 'foreign_id')[0] + (rtable, rcolumn) = db._lookup_constraint_references(main_table, cname) + self.assertEquals(rcolumn, 'rfi_id') + db.delete_table(main_table) + db.delete_table(ref_table) + + def test_rename_constrained_table(self): + """Renames a table with a foreign key column (towards another table)""" + if db.backend_name != "mysql": + return + main_table = 'test_rn_table' + ref_table = 'test_rt_ref' + renamed_table = 'test_renamed_table' + self._create_foreign_tables(main_table, ref_table) + db.execute_deferred_sql() + constraints = db._find_foreign_constraints(main_table, 'foreign_id') + self.assertEquals(len(constraints), 1) + db.rename_table(main_table, renamed_table) + db.execute_deferred_sql() #Create constraints + constraints = db._find_foreign_constraints(renamed_table, 'foreign_id') + self.assertEquals(len(constraints), 1) + (rtable, rcolumn) = db._lookup_constraint_references( + renamed_table, constraints[0]) + self.assertEquals(rcolumn, 'id') + db.delete_table(renamed_table) + db.delete_table(ref_table) + + def test_renamed_referenced_table(self): + """Rename a table referred to in a foreign key""" + if db.backend_name != "mysql": + return + main_table = 'test_rn_refd_table' + ref_table = 'test_rrt_ref' + renamed_table = 'test_renamed_ref' + self._create_foreign_tables(main_table, ref_table) + db.execute_deferred_sql() + constraints = db._lookup_reverse_constraint(ref_table) + self.assertEquals(len(constraints), 1) + db.rename_table(ref_table, renamed_table) + db.execute_deferred_sql() #Create constraints + constraints = db._find_foreign_constraints(main_table, 'foreign_id') + self.assertEquals(len(constraints), 1) + (rtable, rcolumn) = db._lookup_constraint_references( + main_table, constraints[0]) + self.assertEquals(renamed_table, rtable) + db.delete_table(main_table) + db.delete_table(renamed_table) + diff --git a/lib/python/south/tests/db_mysql.pyc b/lib/python/south/tests/db_mysql.pyc new file mode 100644 index 0000000000000000000000000000000000000000..da3fb1bc372c06caefd01d584642da6dfd89245a GIT binary patch literal 6413 zcmchb?QR>#6^3U?iXvsZR_$1+3b$P}NkO-<)B;6|0%@Amv4MUm$x8lFHNaxIJCxQU zcbT1GEdtUn_XdjIL4WiXdISB}n<bQre9i=_>tgl9%N;cGAlzO4mACI?G(p86pO|+5z7e50JDcggYWvR8=*a16Y zTQJGcGT({Lbdkn(1L?`!d_ErVrQL#w^DNV`P4mol`8H4V*x0*B9-*c`%nv{P{l`;X zM9g98k+R8O<1(i(T5*6F-*JmWMn5q=jFpLwO4^dxP)SD;-Eu4#C&s}%J(X-qqOX#k zBrwFjByOl=OAvqI<_vOH-!Z00AsMmc2(fu^L$96GGj+Y)g@j{KHTUZDCn5;at3 zZB^VQT8Cik5-@bsB^(@VV7bBM1yX>{BTWr8e8W5&!Vka#lwGw8^axW|VL4n8a07bq zw{r>BP@<O6xLx#$c9mTrn3@sae5mSHndPT6G_YnzK3k~4H$i{;~9ut zluEmc7jPC-QMHM1V^BWLtg#$QG&Gj@o<`O!}8-iKbuX%rX+jq z*fmlF5ldITFd#OLd2u8%?>w3_R2M9tl>xBqkxb%3Gold8QWrDpcad+Sez$P-%q)!W zZM-$#g;A~BUdQX=cgx$xwF9Tw#_t`ZZn@~dK+h?CgvA-C`VSooV`-`&HjF1oePh;voGfSUzhU}Q1BgjwYaaXy|+GC$4y$S)c4p`HFf zpYQo;@_=qK%4bt7~O+P5~pAm^Q=dy8*qIM3!U0fpgK8g(32?0NNiz&{#upPXE zl;t8-999)Kpac(?`$sTN#)(YuF3m$2k@+-}-biqdg)1rms#*Y%cW$`rr;G?)nHJa3 zf>ZqrISj|SOxhjqu8>wzMrW-l`zLOeDZ`co5=U7EBG5y?0eeM^A)TcdyI|fSCS(hF zH>kxFTU5|Uso(>huO#^Tat(Hn?nZ)fQ)0($6W!%NsIinbnVqD_r5?;61j&eI<5dY~{-d3EoGp;8!r~7l@jMvKKWCPtlyY52Ie9 z;ciLALj0+DXu(mId05)8y;2z#qt22tG#)Ooghz|b)jXgQyjdwPO(ntu_HW3CGs!L@ z8~g6|c_KQWp@KTrVb3S$m`aSU+2=77n7_kp`Q^`1v*EL++IOq%3pBF#%l+Zpk1}7M zTU}()xXKaZ*-#@RAxZY?`W`1Is3kJ`U4J;UK6XvHKQnqX8~c&*jn&g>nw=n#XWF;9 ztAI~rS7e%s$^gnO`xWTV!ZaH)!Zw@EVw8#^(m_NK<;qd_*Iv`9r2w1=5=QfN{z-=eBi}@xsmG+Pxy66lz?Cdjz$yEQgf??0DaTG_G=k zFHr0iZUA;mt$J0Mbh_rwBrZ&XO*so!BubcEIH_DwM%lR06RxWPo`5)v!+KBcBJT?B z`*je8e@L8`^9^nm_H@GihZuYyyc$0$Pc_d|dy1RItz78>1DijOiUe7+9G+3Q0uM?o zDUc4!YCq2a>7H=2Ht;i_tl19a=4?(8a_rXVE^74hd8DM2Ukocsk-c1m z1>wZ$nXE3HQbf-AP=xgVP|pMQ^flD;W9IvsflN3scT_Lu;;-n%N+53wgjazah}ZW} zB$TgIVJP1;LVg;XjWuqm{$ZHpaTo>^gn%yV4sJhT>K8O0&^)H0o(G@M9KkgHD5*7l zzzw>nntd@0r983sJN?{Ui1^~Z^C8q!Y literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/deps_a/migrations/0001_a.py b/lib/python/south/tests/deps_a/migrations/0001_a.py new file mode 100644 index 0000000..d27ed3a --- /dev/null +++ b/lib/python/south/tests/deps_a/migrations/0001_a.py @@ -0,0 +1,11 @@ +from south.db import db +from django.db import models + +class Migration: + + def forwards(self): + pass + + def backwards(self): + pass + diff --git a/lib/python/south/tests/deps_a/migrations/0001_a.pyc b/lib/python/south/tests/deps_a/migrations/0001_a.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c7bbf0351d815f12b7611b4a06ded98bedef6abf GIT binary patch literal 875 zcmcgq%}&EG40f7rut`We^8~%HjMFX%Avl9js~sqZC|$Cx(4VQ!1f1cGc>u7}4X#{9 zRJ;C*{l&KYJQ>aOrz=NkVOoJ z1qn$SbJ7I|@9Bmw^@ve;8j9V;q(N7g9q7tsXJR`h#MB&Jiz;tTZJS!9g?Fg@_SM}YejyK%DN3|qpCf6RMYA7T$>5S9#h(7)4rJ2BEJ?HDTu#gO?U=} z$I0!_&Eh!#Wn~96=2nGQXkDAiY3)a3(VE7#ElEu{An6nP1sS9Rbu{4yhWRlYL_@h7 qEJFxD!)PWp=ZQktEBj>XRl_D;kC5Sizy>@QU8^p+c38==7)Rf%)5Kr^ literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/deps_a/migrations/0002_a.py b/lib/python/south/tests/deps_a/migrations/0002_a.py new file mode 100644 index 0000000..d27ed3a --- /dev/null +++ b/lib/python/south/tests/deps_a/migrations/0002_a.py @@ -0,0 +1,11 @@ +from south.db import db +from django.db import models + +class Migration: + + def forwards(self): + pass + + def backwards(self): + pass + diff --git a/lib/python/south/tests/deps_a/migrations/0002_a.pyc b/lib/python/south/tests/deps_a/migrations/0002_a.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9b73896b3122b3d7b98286786e35c3a74958691d GIT binary patch literal 875 zcmcgq%}&EG40f7rut`We^8~%HOk8$B2*DYITJ1nNMCp=kh5k%+Cg2Qj%maX(ZgAx? zqT2Oe>@T+E=gDZMAMY2iJCgoaL^va6=@U=_R01*qg7pZ>2r3DgBwd0XfbGEtz&pT0 z)&tvzG(Muc#TT7ekmMsym3Q)h_As8I}_V6A*SZ&T2y&!YTMK*ExbeJx3A7N zXJwHqrp=pQlMkD%Un}x^RMu@!8&&Pmqnb{q=h{pt_L$NxoA$-D7WuWvNJ0D^Yr-=) zJWg(ZZWhl0C@VXlF}EtbLhIU8PHR6Ri`F!@ZAogv0ZE_OFUTMrsG|utFwBqHAR5Zu rU>QOH8b&j*IZqVAUfCy8uNpS-dV~!B12*8f=vsBjwZlq|#W?x~u~x)n literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/deps_a/migrations/0003_a.py b/lib/python/south/tests/deps_a/migrations/0003_a.py new file mode 100644 index 0000000..d27ed3a --- /dev/null +++ b/lib/python/south/tests/deps_a/migrations/0003_a.py @@ -0,0 +1,11 @@ +from south.db import db +from django.db import models + +class Migration: + + def forwards(self): + pass + + def backwards(self): + pass + diff --git a/lib/python/south/tests/deps_a/migrations/0003_a.pyc b/lib/python/south/tests/deps_a/migrations/0003_a.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ce17047229b3b94be6a169be35806a392f16664b GIT binary patch literal 875 zcmcgq%}&EG40f7rut`We^8~%HOyaN$LI}w}BQ`@FiY2h6zzkPMK zIV+1?F>T)bnta%7{aTUVqq1&;+Nf%e9@TU@J=bPJvB#8l*|aaFwaBkUMhfEZSQDPX z;c;^NbF+93Kv~%Vjk#6f6Mc1lJt{ql#EXL6{wb{gI literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/deps_a/migrations/0004_a.py b/lib/python/south/tests/deps_a/migrations/0004_a.py new file mode 100644 index 0000000..e5c2977 --- /dev/null +++ b/lib/python/south/tests/deps_a/migrations/0004_a.py @@ -0,0 +1,13 @@ +from south.db import db +from django.db import models + +class Migration: + + depends_on = [('deps_b', '0003_b')] + + def forwards(self): + pass + + def backwards(self): + pass + diff --git a/lib/python/south/tests/deps_a/migrations/0004_a.pyc b/lib/python/south/tests/deps_a/migrations/0004_a.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2fedf34ddf9940e43f655cd440f038d93a2da39b GIT binary patch literal 938 zcmcgqJx{|h5WRe~&?2P%2bL_~%PFs6JANFhh>GBVK0;tjOrybW<2 zB^mL6wn33Mx;CyBlfqjRe*I>RJ1vV`@LIk5CGDYG`=y|Lk0L11qS_^gf&`r_J*2aT zbnV@T+E=gDZMAMY2iJCgoaL^va6=@U=_R01*qg7pZ>2r3DgBwd0XfbGEtz&pT0 z)&tvzG(Muc#TT7ekmMsym3Q)h_As8I}_V6A*SZ&T2y&!YTMK*ExbeJx3A7N zXJwHqrp=pQlMkD%Un}x^RMu@!8&&Pmqnb{q=h{pt_L$NxoA$-D7WuWvNJ0D^Yr-=) zJWg(ZZWhl0C@VXlF}EtbLhIU8PHR6Ri`F!@ZAogv0ZE_OFUTMrsG|utFwBqHAR5Zu rU>QOH8b&j*IZqVAUfCy8uNpS-dV~!B12*8f=vsBjwZlq|#W?x~zTd=f literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/deps_a/migrations/__init__.py b/lib/python/south/tests/deps_a/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/python/south/tests/deps_a/migrations/__init__.pyc b/lib/python/south/tests/deps_a/migrations/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9970d6a31a77c32f2d06b0d204f83a2c17011748 GIT binary patch literal 171 zcmZ9FK?(vf3`Hxt5W#!QMzeGy;t2*}bmby+OmPA;?a-v4=k@}o;L5=Jf&atL^0VC> z@P2u?-z;pO4Oy9xb6r!NwlQ&kB3JX^CLs&-*+)tdI@Tbdg!6Qge#+ WfC@N(X4wF_r8%i~ARCK;m;nH7lO*{7 literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/deps_b/__init__.py b/lib/python/south/tests/deps_b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/python/south/tests/deps_b/__init__.pyc b/lib/python/south/tests/deps_b/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4be05a2888b204ac712627baa56fda7d8667a2fe GIT binary patch literal 160 zcmZSn%*$mSA03d)00oRd+5w1*S%5?e14FO|NW@PANHCxg#hySh{m|mnqGJ7|qQtzE z{5<{O%#ze%{gTp()RcVPoXjMB5H6@J$pFd~=a-gb=mX_TiuF@c3yR~D^yA|*^D;}~ Z?{Ui1^~aSC8z)Z literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/deps_b/migrations/0001_b.py b/lib/python/south/tests/deps_b/migrations/0001_b.py new file mode 100644 index 0000000..d27ed3a --- /dev/null +++ b/lib/python/south/tests/deps_b/migrations/0001_b.py @@ -0,0 +1,11 @@ +from south.db import db +from django.db import models + +class Migration: + + def forwards(self): + pass + + def backwards(self): + pass + diff --git a/lib/python/south/tests/deps_b/migrations/0001_b.pyc b/lib/python/south/tests/deps_b/migrations/0001_b.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f66a123350ca37daf4c60b3cebebaa408daaf06c GIT binary patch literal 875 zcmcgq%}&EG40f7rut`We^8~%HjMFX%Avl9js~sqZs9CbD(4VQ!1f1cGc>u7}4X#{9 zRJ;C*{l&KYJQ>aOrz=NkVOoJ z1qn$SbJ7I|@9Bmw^@ve;8j9V;q(N7g9q7tsXJR`h#MB&Ji^{ZlZJS!9g?Fg@_SM7qBV_eTaub^K+-4n3o=Lt>S)3Z4D(|)h=y`E rScVXQhS5x{;fX@nEBln!tAj9s?XZ$#F^;|gwh_c= literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/deps_b/migrations/0002_b.py b/lib/python/south/tests/deps_b/migrations/0002_b.py new file mode 100644 index 0000000..459ea5d --- /dev/null +++ b/lib/python/south/tests/deps_b/migrations/0002_b.py @@ -0,0 +1,13 @@ +from south.db import db +from django.db import models + +class Migration: + + depends_on = [('deps_a', '0002_a')] + + def forwards(self): + pass + + def backwards(self): + pass + diff --git a/lib/python/south/tests/deps_b/migrations/0002_b.pyc b/lib/python/south/tests/deps_b/migrations/0002_b.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8a28db1c6a4ceffe19a001ed60f69d64b2d7f8a7 GIT binary patch literal 938 zcmcgqJx{|h5WRe~&?2P%2bL_<6-yU{5X>Otih(d#PVKgVB(8Fi!IY@dX_m$Yz7#?Tu<1yDzjM-Z_eLlr}vK%S7Ffa!xt;RE0u z;2}@J^k9}9(7)vuJy}G^$9z@M;XdKo8C#(r5WFdut@fpBegyF0D_VF$#sc#a!IA{T zOioP1OvDUQkh82u=R99)>>8C0a59;kDLrTQ-MS`>DIWt;$kE%340N)118ueFLL5g) zM%<%qQ53CijBCWK^cIERyjtTGM1gN%_AhUo8FB`3Upo!p&5&!2{G5nVjjpXyei$5T@}}Ck zR1n)3RIZWSDaix!n?gW)s<&jhp}S1nj|cG}*)~254l56>8=HbV4q+_KlWvxdLp&(q Vj{lJx#^b!J-34=pksU>&_#1s}&=&vz literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/deps_b/migrations/0003_b.py b/lib/python/south/tests/deps_b/migrations/0003_b.py new file mode 100644 index 0000000..1692888 --- /dev/null +++ b/lib/python/south/tests/deps_b/migrations/0003_b.py @@ -0,0 +1,13 @@ +from south.db import db +from django.db import models + +class Migration: + + depends_on = [('deps_a', '0003_a')] + + def forwards(self): + pass + + def backwards(self): + pass + diff --git a/lib/python/south/tests/deps_b/migrations/0003_b.pyc b/lib/python/south/tests/deps_b/migrations/0003_b.pyc new file mode 100644 index 0000000000000000000000000000000000000000..43761fb3848ab5c856679bf767cb8eb0686e1527 GIT binary patch literal 938 zcmcgqJx{|h5WRe~&?2P%2bL_i=t?CV_YLj=G7W^T9t+1wSMy}+C#VTD?$4nMNm?RdY2rEAgCDA*+aT^ z;o2A78f1)|FhqaXDmmf(>*Vefdj1@Q9+9&Znz3G)u;HS0<>y3{YIJRt^26XzlQ-4Q zrGnVTpmL4mPDvh+-xLDcQ@th24c%qpemsZ=$+q!fa9DY0-Pjb|aR_5+o^-Qx9O6L< Vcl?jsFdpY!?Jk%*jO-{H#ovVi&>R2& literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/deps_b/migrations/0004_b.py b/lib/python/south/tests/deps_b/migrations/0004_b.py new file mode 100644 index 0000000..d27ed3a --- /dev/null +++ b/lib/python/south/tests/deps_b/migrations/0004_b.py @@ -0,0 +1,11 @@ +from south.db import db +from django.db import models + +class Migration: + + def forwards(self): + pass + + def backwards(self): + pass + diff --git a/lib/python/south/tests/deps_b/migrations/0004_b.pyc b/lib/python/south/tests/deps_b/migrations/0004_b.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e9d87d6cc29252a39bf1a790618f22f3b1f7cc82 GIT binary patch literal 875 zcmcgq%}&EG40f7rut`We^8~%H?0~cjLI}w}BqL68(_G+hWz`zG&(n0DR+`2v3PQ;JQ?n3S<$3 zVL?KY#+-D)!F#&lOFd!~o`zy~F=^1%We2))*_qgm2{AQC*P=3QUfZTtY2h6zzkPMK zIV%gJm^N>IO+IY4eyzywQCYV^ZB(^Kk7_!do@+Cq*kek&Y}%K+waBkUMhfEZSQDPX z;c;^NO}=;zKv~%Vjk#6f6|X+m@sz9FX*h{eleAfjXLS1H=574Wgmk r4VEDUpkXu_6dTsy4fSd61@!@T+E=gDZMAMY2iJCgoaL^va6=@U=_R01*qg7pZ>2r3DgBwd0XfbGEtz&pT0 z)&tvzG(Muc#TT7ekmMsyWjc94a@AN@q92lYTddmL7ft;GfNxwA;VCf(T$jpHfh=M$ zEJ#Swn3FCzcuzNcsYi^$(@^X#CJnl}>_As8I}_V6A*SZ&T2!XZYunT+ExbeJx3A7N zXJug&)8@^u$%oC>uNC<{D(g0=jjHzOQB9}Qb8RLRdrWDUP5Y9!7WuWvNJ0D^Yr-=) zJWg)E$rsN7C@VXlF}EtbLhCxOoYsCs7OiP)+mh6T1Clm3q4NnxpUfHL-UNvmu^#~dM2W-G|(Y5N5YloE_i*fV~$Q{IZ literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/deps_b/migrations/__init__.py b/lib/python/south/tests/deps_b/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/python/south/tests/deps_b/migrations/__init__.pyc b/lib/python/south/tests/deps_b/migrations/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..90e4c0a9a32637dc0985ab91302c7f91c0c04b5c GIT binary patch literal 171 zcmZ9FK?(vf3`Hxt5W#!QMzeGy;t2*}bmbyUZE*rK&CsNv=k@}o;L5=Jf&atL^0VC> z@P2u?-z;pO4Oy84=enjkZDZp8M6Tw+O=1@Cxle%;bfi&0%PR)njpU&+zjd%oxaa_u jt3ZgepofW`xmV&OfJliHac|daD?Uyf${0`ey%_EbwaF?U literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/deps_b/models.py b/lib/python/south/tests/deps_b/models.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/python/south/tests/deps_b/models.pyc b/lib/python/south/tests/deps_b/models.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f7de63e42272fbd2153c0c61aed4658de2c829bc GIT binary patch literal 158 zcmZSn%*$mSA03d)00oRd+5w1*S%5?e14FO|NW@PANHCxg#qK~c{m|mnqGJ7|qQtzE z{5<{O%#ze%{gTp()RcVPoXjMB5H6@J$pFd~=a-gb=mX_TiuF@c3yR~D^mFr5Qge#+ WfC@N(X4wF_r8%i~ARCK;m;nH7v?Tff literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/deps_c/__init__.py b/lib/python/south/tests/deps_c/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/python/south/tests/deps_c/__init__.pyc b/lib/python/south/tests/deps_c/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e711f23a9cd67d6eeabf3126b4f2028ee807635c GIT binary patch literal 160 zcmZSn%*$mSA03d)00oRd+5w1*S%5?e14FO|NW@PANHCxg#hySh{m|mnqGJ7|qQtzE z{5<{O%#ze%{gTp()RcVPoXjMB5H6@J$pFd~=a-gb=mX_TiuF@c3yR~D_2c6+^D;}~ Z?{Ui1^~a#C8+=a literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/deps_c/migrations/0001_c.py b/lib/python/south/tests/deps_c/migrations/0001_c.py new file mode 100644 index 0000000..d27ed3a --- /dev/null +++ b/lib/python/south/tests/deps_c/migrations/0001_c.py @@ -0,0 +1,11 @@ +from south.db import db +from django.db import models + +class Migration: + + def forwards(self): + pass + + def backwards(self): + pass + diff --git a/lib/python/south/tests/deps_c/migrations/0001_c.pyc b/lib/python/south/tests/deps_c/migrations/0001_c.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d5bbebe93a282910c6244bfd271ea1e56416f4f2 GIT binary patch literal 875 zcmcgq%}&EG40f7rut`We^8~#>#%ULX5S&4%6$i>8YMX2;w9Qm!0?zQpJOJ3~23Ia4 zs$KuZ{$g8x9!=)@@vemJk@UYL!Z|TRpMV;m5s(QGtVd8s&`8K6=@QHc%m6+B-T@x6 z0hl4A@d4efzUah)Bp-3AqL)V`H_q4^{g}j?YT4z!a_tWQzHvo_$HXjfT`EfjvWUU3 zAR$R(PFiyCj;{IAfEb0Rq1bIq8uWG9fwo@sCbnZjOx>gHP!(O?8rQ0{@)nifyjtT< z>Z(vo>o>n5AG(cSDe`+%#;#G9s@b7OHJi=Oba_g#hm>~Vx|h5&$gf343gYir6Q05T zadP{MynGHoS=j=OxmDp6TDN&)we}OTXia0=)TAaHkPM0af(+80I+}0;!~B?yqOsf# qmLUY7VKk?v;E6)mEAy1MOUEW&kC5Sizy>_$eXB0Gc38=&m`2~fPsDHl literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/deps_c/migrations/0002_c.py b/lib/python/south/tests/deps_c/migrations/0002_c.py new file mode 100644 index 0000000..d27ed3a --- /dev/null +++ b/lib/python/south/tests/deps_c/migrations/0002_c.py @@ -0,0 +1,11 @@ +from south.db import db +from django.db import models + +class Migration: + + def forwards(self): + pass + + def backwards(self): + pass + diff --git a/lib/python/south/tests/deps_c/migrations/0002_c.pyc b/lib/python/south/tests/deps_c/migrations/0002_c.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bd076b32971adb3de54fcbc265ddfa5cf14a05f0 GIT binary patch literal 875 zcmcgq%}&EG40f7rut`We^8~#>CN8@mgy0N9tvFB)QQKr&p>3u*6L5w%<^jM?H@I>c zQSJIK_7~go^Jp^Hk9Q?(kEH)45zdJj`UKPfjetymU_F95f<{6nNta+oU9L_6}>znxpBtU=*J}9RLd^+m1}rz=NkVOoJ z1qn$SbJCK7cXZ8{2E-^l4aII_(x9))4z%^6H?bWPV(K1khpOoE*0@%smA9z;=G7W^ zQdfmyTEF=f`Ot0rN|E2AGIou+RLu@Os@ZIIrpr@`J*2b?*S+MOL4GYVQV@T~n(z$v zkCWS9CUMvWAp~a-YQ=$ah}tIG3T-phnSe9AF%JNCy1|vp zh-%k=vA@`spGT9qe!MGTdnElYiEvKL&?le>;IHxb7wI4DxG{k%IU;)`VxU zf1KR@A}^ldEcrFt{ql#DyGpl$W_F6 literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/deps_c/migrations/0004_c.py b/lib/python/south/tests/deps_c/migrations/0004_c.py new file mode 100644 index 0000000..d27ed3a --- /dev/null +++ b/lib/python/south/tests/deps_c/migrations/0004_c.py @@ -0,0 +1,11 @@ +from south.db import db +from django.db import models + +class Migration: + + def forwards(self): + pass + + def backwards(self): + pass + diff --git a/lib/python/south/tests/deps_c/migrations/0004_c.pyc b/lib/python/south/tests/deps_c/migrations/0004_c.pyc new file mode 100644 index 0000000000000000000000000000000000000000..24454103779f75018354ca21a282447b566ef8fc GIT binary patch literal 875 zcmcgq%}&EG40f7rut`We^8~#>c0k$%Ap~a-YQ=$ah}tIG3T-phnSe9AF%JNCy1|vp zh-%k=vA@`spGT9qe!MGTdnElYiEvKL&?le>;IHxb7wI4DxG{k%IU;)`VxU zf1KR@A}^ldEcrFt{ql#DyGpl%-FsM>saaHDm*XqqLX%F4nF9q#;6hTQL>TPl;NYI%oPU!4D zT|0N}i*5}vMot)_ziXA8@a}bTcXC}k2cZY#Y=ma4S0-$@XkGa^5v3YkTc!LkIMn2I zwRNc=wlSz&Be_$O2jn+}fOb@G$#O$?nYbSh;z6=$d>9;79$I%|a_%^Uu{2M*SvU^y XpoBaAM{XF8^R9Lm%pFE{6pi9ht345>`Kb4^{GYi;8GMDFInLqZbhQX4%-Xh?&A<|TOEkJLe7e(PYFaMl4V jR{g!6Qge#+ WfC@N(X4wF_r8%i~ARCK;m;nH7)g=1> literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/emptyapp/__init__.py b/lib/python/south/tests/emptyapp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/python/south/tests/emptyapp/__init__.pyc b/lib/python/south/tests/emptyapp/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7132a27e90c63016c708ee2fe52836bb85279c55 GIT binary patch literal 162 zcmZSn%*$mSA03d)00oRd+5w1*S%5?e14FO|NW@PANHCxg#ojz3kvk(<1_Oz bOXB18fQmSP=Go-tr24V&PA8016 literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/emptyapp/migrations/__init__.py b/lib/python/south/tests/emptyapp/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/python/south/tests/emptyapp/migrations/__init__.pyc b/lib/python/south/tests/emptyapp/migrations/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b31e83062162160bd34a9773830dc032053523e8 GIT binary patch literal 173 zcmZ9GO$q`r423JY5W#!QMzeGy;t50;bmJm)Y;gk9ADR@Lb9(_(aAn|of%n2A%g=Um zz{mAzezVYhR(Pf2jOn}PTsjr=Cwx~AZtM%gP;7K5LQmouq`tjNyNL#9z3kvje^HWlD YiuHg>IDn?v0J)_(sdgYci-DK{0LxS*{{R30 literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/fakeapp/__init__.py b/lib/python/south/tests/fakeapp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/python/south/tests/fakeapp/__init__.pyc b/lib/python/south/tests/fakeapp/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7a608ea0d6e95f95d6f6d234dfd83871d5058a92 GIT binary patch literal 161 zcmZSn%*$mSA03d)00oRd+5w1*S%5?e14FO|NW@PANHCxg#a=)${m|mnqGJ7|qQtzE z{5<{O%#ze%{gTp()RcVPoXjMB5H6@J$pFd~=a-gb=mX_TiuKbHvr`ib3iRXSGxIV_ a;^XyzYB+$#+2rP@l;)(`fh;WsVg>-~z9tj^ literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/fakeapp/migrations/0001_spam.py b/lib/python/south/tests/fakeapp/migrations/0001_spam.py new file mode 100644 index 0000000..9739648 --- /dev/null +++ b/lib/python/south/tests/fakeapp/migrations/0001_spam.py @@ -0,0 +1,17 @@ +from south.db import db +from django.db import models + +class Migration: + + def forwards(self): + # Model 'Spam' + db.create_table("southtest_spam", ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('weight', models.FloatField()), + ('expires', models.DateTimeField()), + ('name', models.CharField(max_length=255)) + )) + + def backwards(self): + db.delete_table("southtest_spam") + diff --git a/lib/python/south/tests/fakeapp/migrations/0001_spam.pyc b/lib/python/south/tests/fakeapp/migrations/0001_spam.pyc new file mode 100644 index 0000000000000000000000000000000000000000..59d48e71bcc30905dca937d90aa8afa496b10f2c GIT binary patch literal 1271 zcmcgrO>fgc5FOk3Xi`dxKpePn^MyK`dO-+iRfz);atadRuv%v~i4!}Hyc?RF!$0N+ zls99V{(|Co#xuLKGjHBbqCY473-$Bsg#JAdz8@g&0?ObGs30l>8V3}xA5sxg8PPa` zACc*hY0+<@J<*SGi%gqFaTjOvJ3Mg!%ZHpQYwQl}vNE=Cz6bM5KC89QtJMJjpZElE zub?b(U6e(EamZlUh$tP!oOHs$EPco&#Lb~R(E^9?hFp6Akaz`}hwc#co}wx8btRz9 z;MqMAw{!3I#O;#llR3s>fS4)HhQh=seu82{|CH-i8$b8fdF9r+l!kNTdH+3o%b-hzdyOTJ*@s*m?R(oqalXPqI+1z`k(r(vzjpQxZl84(Z^;Q*j zHS_cQ4sLt|l|o3;HJOyxSz%MT1ZMjqid^TmFp}z~-dNA6u8T_hX8i>26)@c7rEOOD z+~r)?;x%{3IGin}F}@_qeTS8NbGCL#R_m3iR>>#_A&K8?t*PD=d6uxOcYcmN;VWUn zTr$;5tJmwKydMRZ3B&->JB8!83kqVUSUOiSIZDYzDTTkz zrm#w-FA$Kvgn9t_*f)_fHo1FNov_y%HpSa;1*}}_xyc%GxjD1Yt69ZMJcN?r{{a?E Q?;6V9bA89a&w{h?FIY1qC;$Ke literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/fakeapp/migrations/0002_eggs.py b/lib/python/south/tests/fakeapp/migrations/0002_eggs.py new file mode 100644 index 0000000..3ec8399 --- /dev/null +++ b/lib/python/south/tests/fakeapp/migrations/0002_eggs.py @@ -0,0 +1,20 @@ +from south.db import db +from django.db import models + +class Migration: + + def forwards(self): + + Spam = db.mock_model(model_name='Spam', db_table='southtest_spam', db_tablespace='', pk_field_name='id', pk_field_type=models.AutoField) + + db.create_table("southtest_eggs", ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('size', models.FloatField()), + ('quantity', models.IntegerField()), + ('spam', models.ForeignKey(Spam)), + )) + + def backwards(self): + + db.delete_table("southtest_eggs") + diff --git a/lib/python/south/tests/fakeapp/migrations/0002_eggs.pyc b/lib/python/south/tests/fakeapp/migrations/0002_eggs.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3cc7438fc6851d6a1d53b1dc98555249be721d8f GIT binary patch literal 1429 zcmcgrOK%fF4EF9OX_LM=0z%@pmv$>Iy&%L(l`2GqHh@GpjAk=Vy3M}WnE*j>3crBA z$`1hBleECSY}SrF6nAr*dlpGVt`642vH+1(d+W9=S@y9r+dSOy$0>A*4+ z&W7y+jFoj>+zVT@EEmC4J^ zOigkujL*na7r2mN@DQ+=q?{!t@yfm>D|utFwMo|M%G6ad%!%E^%|D~5 z4~slYSek{qpgkc!VfJk@(KFOdla%W*waM}E@dL#S9#K4ZZc?|ObZab6wq;f!JPiB6 z{a}xNGNJ1!l?gqdfU60mN#T#mPxE=or==E8AiMOXphHiOd_n0Q4kEev>bO~%9?zsa z#%+1PAdHz{sa?tFC?)+;+~|3MN~L!xAiYQG8mL$|*o&00F)+)j7xuT#{pDS91*|mo j$Yh>eF3w!)YFhIWw@5PlKfr?Np{MK--!H)TgZ=O~4HQgo literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/fakeapp/migrations/0003_alter_spam.py b/lib/python/south/tests/fakeapp/migrations/0003_alter_spam.py new file mode 100644 index 0000000..39126c2 --- /dev/null +++ b/lib/python/south/tests/fakeapp/migrations/0003_alter_spam.py @@ -0,0 +1,18 @@ +from south.db import db +from django.db import models + +class Migration: + + def forwards(self): + + db.alter_column("southtest_spam", 'weight', models.FloatField(null=True)) + + def backwards(self): + + db.alter_column("southtest_spam", 'weight', models.FloatField()) + + models = { + "fakeapp.bug135": { + 'date': ('models.DateTimeField', [], {'default': 'datetime.datetime(2009, 5, 6, 15, 33, 15, 780013)'}), + } + } diff --git a/lib/python/south/tests/fakeapp/migrations/0003_alter_spam.pyc b/lib/python/south/tests/fakeapp/migrations/0003_alter_spam.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ac01f39922766764e8bf241c17768718334b6ef GIT binary patch literal 1257 zcmcgqO>fgc5FLLckn*7(xFSX35~L72L`{l>Dg>xEBIK3>0m)itH;L1=9eLL(oWnon z2f&+kS`OS}d1q(Wo|*UFOz`{3$t?Z$c}a&S!2S-)%wQy5k2F!?(ZVB-&pv6NihvdY z&H>3T$&h{#?TNlFLXsW28azO3{s+&%gJ*qCm9>5seo;xKjqSnxl&@=H^QycDz_IVK z%ms`hu8XoLu<#iSI|1b$zreCaAvnMh(sYHbLsLMS0!h;1STY1&T=@f*c>!aIwuo~% zfNzV)fNTgB-kymOScLd*Zl7{U)!2L=csY)`Je51dHrr7@@7I z(`BWbqD-6yC%C=Tm9Uq&($d9U*NtjL;Vee!)c`v=W4^0{k&g*d`q1gmYRXoY?VCxUG(C!fih$QLdMX<&9&?Z8$JB z&)~PCIQl58y3Pybnq$WJAaP@HGrDsIujBa5uzxn}Plx?6w#lU3XXkM|p1icoP^y(^ zbUPLtXn1-K{TOAFO)Y~;Q$T4njtL)=M{Ia<{z-M5!AN)x+(0sp zuHOrLVc-ScyamE@?$Eo58_BF4dKOu3MY*o{igCL<#|H0^y=|4eU=)U#oO-AJUq}ib A1ONa4 literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/fakeapp/migrations/__init__.py b/lib/python/south/tests/fakeapp/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/python/south/tests/fakeapp/migrations/__init__.pyc b/lib/python/south/tests/fakeapp/migrations/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a8a0e739abbfa03171f2cdbb1f48446187aca322 GIT binary patch literal 172 zcmZSn%*$mSA03d)00oRd+5w1*S%5?e14FO|NW@PANHCxg#i2kk{m|mnqGJ7|qQtzE z{5<{O%#ze%{gTp()RcVPoXjMB5H6@J$pFd~=a-gb=mX_TiuKbHvr`ib3iNX`(~A;I lGV}9__2c6+^D;}~@Nml1_0)ZDvJOB literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/fakeapp/models.py b/lib/python/south/tests/fakeapp/models.py new file mode 100644 index 0000000..b7e8a9b --- /dev/null +++ b/lib/python/south/tests/fakeapp/models.py @@ -0,0 +1,111 @@ +# -*- coding: UTF-8 -*- + +from django.db import models +from django.contrib.auth.models import User as UserAlias + +from south.modelsinspector import add_introspection_rules + +on_delete_is_available = hasattr(models, "PROTECT") # models here is django.db.models + +def default_func(): + return "yays" + +# An empty case. +class Other1(models.Model): pass + +# Another one +class Other3(models.Model): pass +def get_sentinel_object(): + """ + A function to return the object to be used in place of any deleted object, + when using the SET option for on_delete. + """ + # Create a new one, so we always have an instance to test with. Can't work! + return Other3() + +# Nastiness. +class HorribleModel(models.Model): + "A model to test the edge cases of model parsing" + + ZERO, ONE = range(2) + + # First, some nice fields + name = models.CharField(max_length=255) + short_name = models.CharField(max_length=50) + slug = models.SlugField(unique=True) + + # A ForeignKey, to a model above, and then below + o1 = models.ForeignKey(Other1) + o2 = models.ForeignKey('Other2') + + if on_delete_is_available: + o_set_null_on_delete = models.ForeignKey('Other3', null=True, on_delete=models.SET_NULL) + o_cascade_delete = models.ForeignKey('Other3', null=True, on_delete=models.CASCADE, related_name="cascademe") + o_protect = models.ForeignKey('Other3', null=True, on_delete=models.PROTECT, related_name="dontcascademe") + o_default_on_delete = models.ForeignKey('Other3', null=True, default=1, on_delete=models.SET_DEFAULT, related_name="setmedefault") + o_set_on_delete_function = models.ForeignKey('Other3', null=True, default=1, on_delete=models.SET(get_sentinel_object), related_name="setsentinel") + o_set_on_delete_value = models.ForeignKey('Other3', null=True, default=1, on_delete=models.SET(get_sentinel_object()), related_name="setsentinelwithactualvalue") # dubious case + o_no_action_on_delete = models.ForeignKey('Other3', null=True, default=1, on_delete=models.DO_NOTHING, related_name="deletemeatyourperil") + + + # Now to something outside + user = models.ForeignKey(UserAlias, related_name="horribles") + + # Unicode! + code = models.CharField(max_length=25, default="↑↑↓↓←→←→BA") + + # Odd defaults! + class_attr = models.IntegerField(default=ZERO) + func = models.CharField(max_length=25, default=default_func) + + # Time to get nasty. Define a non-field choices, and use it + choices = [('hello', '1'), ('world', '2')] + choiced = models.CharField(max_length=20, choices=choices) + + class Meta: + db_table = "my_fave" + verbose_name = "Dr. Strangelove," + \ + """or how I learned to stop worrying +and love the bomb""" + + # Now spread over multiple lines + multiline = \ + models.TextField( + ) + +# Special case. +class Other2(models.Model): + # Try loading a field without a newline after it (inspect hates this) + close_but_no_cigar = models.PositiveIntegerField(primary_key=True) + +class CustomField(models.IntegerField): + def __init__(self, an_other_model, **kwargs): + super(CustomField, self).__init__(**kwargs) + self.an_other_model = an_other_model + +add_introspection_rules([ + ( + [CustomField], + [], + {'an_other_model': ('an_other_model', {})}, + ), +], ['^south\.tests\.fakeapp\.models\.CustomField']) + +class BaseModel(models.Model): + pass + +class SubModel(BaseModel): + others = models.ManyToManyField(Other1) + custom = CustomField(Other2) + +class CircularA(models.Model): + c = models.ForeignKey('CircularC') + +class CircularB(models.Model): + a = models.ForeignKey(CircularA) + +class CircularC(models.Model): + b = models.ForeignKey(CircularB) + +class Recursive(models.Model): + self = models.ForeignKey('self') diff --git a/lib/python/south/tests/fakeapp/models.pyc b/lib/python/south/tests/fakeapp/models.pyc new file mode 100644 index 0000000000000000000000000000000000000000..76df93411bb13629f59adda304e4bd70f6514663 GIT binary patch literal 5748 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 XYood`Rly;uVy)S;_C{rmIQaV?jVSrx literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/freezer.py b/lib/python/south/tests/freezer.py new file mode 100644 index 0000000..82c4402 --- /dev/null +++ b/lib/python/south/tests/freezer.py @@ -0,0 +1,15 @@ +from south.tests import unittest + +from south.creator.freezer import model_dependencies +from south.tests.fakeapp import models + +class TestFreezer(unittest.TestCase): + def test_dependencies(self): + self.assertEqual(set(model_dependencies(models.SubModel)), + set([models.BaseModel, models.Other1, models.Other2])) + + self.assertEqual(set(model_dependencies(models.CircularA)), + set([models.CircularA, models.CircularB, models.CircularC])) + + self.assertEqual(set(model_dependencies(models.Recursive)), + set([models.Recursive])) diff --git a/lib/python/south/tests/freezer.pyc b/lib/python/south/tests/freezer.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0e8bd811b51ac87be60764a7bcc846bd98f63cf6 GIT binary patch literal 1062 zcmcIiJ&)5s5S_IXCzpT-AS60kH^hR51|cpHbax=Sozm6kVto$LXHXe_MFml*C{m;ZJ5)GSx)iw` zcBvRpIizR^JRnxI^Jt?e8`3wT1JT=PM43k`e}o(JpZI)*of$*5Rc;Jx(^>AoQq~z4 zamIVDGOp5`b?5Vb`?Q&2dx4;r4f7Xnj@0GW1w`M5ViLw63=)w;;mTf<${7*4ZVA!f zp}rFBKuD2s$hfo{&_T&B7Qms^p4B6|bgkDR^=H_)A2zlDCst4X5;pCJP3h7;e!vT$ z-|;^8Z-A%GJkgpP^WsCB6d;ET*o2Z%=*S5v>Z)CzNfpfrz#`GyAxZg_*>dy99Ji0{ znPuLjZILvmJ+$bdr9~m9ZKLy#+%;u@D>WAzA1hgmvm5!y+M%q(NYIe_tgWZ6JKgz(~jdRDLIbK z1eQ3)AhiWxcnk2F;6G#;%F|LxEv5I=xj)8gn`G0P+q6R1$lQbKjA_G(shjy#wzhZX r?$K>y=9}a_C;R<%PcKl6C0c3U{4d$q8b0l+_Dlv!OKLM`re@AB6IJX) literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/inspector.py b/lib/python/south/tests/inspector.py new file mode 100644 index 0000000..dcd6d57 --- /dev/null +++ b/lib/python/south/tests/inspector.py @@ -0,0 +1,109 @@ + +from south.tests import Monkeypatcher, skipUnless +from south.modelsinspector import (convert_on_delete_handler, get_value, + IsDefault, models, value_clean) + +from fakeapp.models import HorribleModel, get_sentinel_object + + +on_delete_is_available = hasattr(models, "PROTECT") # models here is django.db.models +skipUnlessOnDeleteAvailable = skipUnless(on_delete_is_available, "not testing on_delete -- not available on Django<1.3") + +class TestModelInspector(Monkeypatcher): + + """ + Tests if the various parts of the modelinspector work. + """ + + def test_get_value(self): + + # Let's start nicely. + name = HorribleModel._meta.get_field_by_name("name")[0] + slug = HorribleModel._meta.get_field_by_name("slug")[0] + user = HorribleModel._meta.get_field_by_name("user")[0] + + # Simple int retrieval + self.assertEqual( + get_value(name, ["max_length", {}]), + "255", + ) + + # Bool retrieval + self.assertEqual( + get_value(slug, ["unique", {}]), + "True", + ) + + # String retrieval + self.assertEqual( + get_value(user, ["rel.related_name", {}]), + "'horribles'", + ) + + # Default triggering + self.assertEqual( + get_value(slug, ["unique", {"default": False}]), + "True", + ) + self.assertRaises( + IsDefault, + get_value, + slug, + ["unique", {"default": True}], + ) + + @skipUnlessOnDeleteAvailable + def test_get_value_on_delete(self): + + # First validate the FK fields with on_delete options + o_set_null_on_delete = HorribleModel._meta.get_field_by_name("o_set_null_on_delete")[0] + o_cascade_delete = HorribleModel._meta.get_field_by_name("o_cascade_delete")[0] + o_protect = HorribleModel._meta.get_field_by_name("o_protect")[0] + o_default_on_delete = HorribleModel._meta.get_field_by_name("o_default_on_delete")[0] + o_set_on_delete_function = HorribleModel._meta.get_field_by_name("o_set_on_delete_function")[0] + o_set_on_delete_value = HorribleModel._meta.get_field_by_name("o_set_on_delete_value")[0] + o_no_action_on_delete = HorribleModel._meta.get_field_by_name("o_no_action_on_delete")[0] + # TODO this is repeated from the introspection_details in modelsinspector: + # better to refactor that so we can reference these settings, in case they + # must change at some point. + on_delete = ["rel.on_delete", {"default": models.CASCADE, "is_django_function": True, "converter": convert_on_delete_handler, }] + + # Foreign Key cascade update/delete + self.assertRaises( + IsDefault, + get_value, + o_cascade_delete, + on_delete, + ) + self.assertEqual( + get_value(o_protect, on_delete), + "models.PROTECT", + ) + self.assertEqual( + get_value(o_no_action_on_delete, on_delete), + "models.DO_NOTHING", + ) + self.assertEqual( + get_value(o_set_null_on_delete, on_delete), + "models.SET_NULL", + ) + self.assertEqual( + get_value(o_default_on_delete, on_delete), + "models.SET_DEFAULT", + ) + # For now o_set_on_delete raises, see modelsinspector.py + #self.assertEqual( + # get_value(o_set_on_delete_function, on_delete), + # "models.SET(get_sentinel_object)", + #) + self.assertRaises( + ValueError, + get_value, + o_set_on_delete_function, + on_delete, + ) + self.assertEqual( + get_value(o_set_on_delete_value, on_delete), + "models.SET(%s)" % value_clean(get_sentinel_object()), + ) + \ No newline at end of file diff --git a/lib/python/south/tests/inspector.pyc b/lib/python/south/tests/inspector.pyc new file mode 100644 index 0000000000000000000000000000000000000000..de4ae0fa25018b59fb1012cd840c0573e4793909 GIT binary patch literal 2796 zcmcImOK;mo5FUzpS+?UOZQZ)B=+HE3fk@~h$t5V_+D;Q7u>)EzQ1!5&#g#>y4~4sQ zV<0)D$Ntpb`UBc;h7=_my|od&zX+K%4|0K<_@Ho=Xd7OQ(^k#Ho!$e0aJwDZAYg^F2 z@D7H1J45q5`blcLRvU~t=~jE{-o4vpgMaRakq@6Rb$3twtV$b)gcF@e95SHlFp;Orc*2LmlZ=5Bq z%R3QmZAlAUlv5vJ7$*P;q_Z$X)mk#Qub5p5R+h}2E9PQ0w$`9v zmF|~oESxk6Ddb&L1KZWWy1@Lfw}u1nV+^wE#~M4u`T5O6`H6ILQzA6>U#UnZCw6K% z0pH%cCs50i@Fg(FbB0;2&2@A#9re+Cs{_Su4FmDcv;>{GBghI0yh?d^3^g2sNX6Rv z_6B;4?@6emKpkI*7lE^Hct3kDb3gKU8#3v!9~o`=D23VcL!*tyd-B*-k9)SuC-p4FHVeu)8&scoU;uec8M3JI-D&GMc@IEfH!6H!)U^9IGQC)a}xq+Yh7hs5j z378Ysgc+{L*5wgBK0b;I94d6Fl*C6Vq$7_sUOBKz1?gBRkH0T0$7S*1agglFSH|}I zCinPGUIavX{})Gk{L#Z4Y?6Qb4RC-2x&4 z41oyaNZ=q;Jn_%8Kb!T7Nd@LOP7;u*Z+vSry9q^AEeb_aeb9Jcvp3%FJBo!Q?O~Co T9~kV07u$4t?xwrpY}Eb%u`p-I literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/logger.py b/lib/python/south/tests/logger.py new file mode 100644 index 0000000..7a90277 --- /dev/null +++ b/lib/python/south/tests/logger.py @@ -0,0 +1,82 @@ +import logging +import os +import tempfile +from south.tests import unittest +import StringIO +import sys + +from django.conf import settings +from django.db import connection, models + +from south.db import db +from south.logger import close_logger + +class TestLogger(unittest.TestCase): + + """ + Tests if the logging is working reasonably. Some tests ignored if you don't + have write permission to the disk. + """ + + def setUp(self): + db.debug = False + self.test_path = tempfile.mkstemp(suffix=".south.log")[1] + + def test_db_execute_logging_nofile(self): + "Does logging degrade nicely if SOUTH_LOGGING_ON not set?" + settings.SOUTH_LOGGING_ON = False # this needs to be set to False + # to avoid issues where other tests + # set this to True. settings is shared + # between these tests. + db.create_table("test9", [('email_confirmed', models.BooleanField(default=False))]) + + def test_db_execute_logging_off_with_basic_config(self): + """ + Does the south logger avoid outputing debug information with + south logging turned off and python logging configured with + a basic config?" + """ + settings.SOUTH_LOGGING_ON = False + + # Set root logger to capture WARNING and worse + logging_stream = StringIO.StringIO() + logging.basicConfig(stream=logging_stream, level=logging.WARNING) + + db.create_table("test12", [('email_confirmed', models.BooleanField(default=False))]) + + # since south logging is off, and our root logger is at WARNING + # we should not find DEBUG info in the log + self.assertEqual(logging_stream.getvalue(), '') + + def test_db_execute_logging_validfile(self): + "Does logging work when passing in a valid file?" + settings.SOUTH_LOGGING_ON = True + settings.SOUTH_LOGGING_FILE = self.test_path + # Check to see if we can make the logfile + try: + fh = open(self.test_path, "w") + except IOError: + # Permission was denied, ignore the test. + return + else: + fh.close() + # Do an action which logs + db.create_table("test10", [('email_confirmed', models.BooleanField(default=False))]) + # Close the logged file + close_logger() + try: + os.remove(self.test_path) + except: + # It's a tempfile, it's not vital we remove it. + pass + + def test_db_execute_logging_missingfilename(self): + "Does logging raise an error if there is a missing filename?" + settings.SOUTH_LOGGING_ON = True + settings.SOUTH_LOGGING_FILE = None + self.assertRaises( + IOError, + db.create_table, + "test11", + [('email_confirmed', models.BooleanField(default=False))], + ) diff --git a/lib/python/south/tests/logger.pyc b/lib/python/south/tests/logger.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4739c963f48ad8f5a5ea75278eb8ebb4528ba9c9 GIT binary patch literal 3326 zcmcImZEqVz5S~3dcI+fgT1upXgcd}pk!akO4}d_VG<_jb<)Cw_7W3hB_O0!6?#0~h zC5==*l^?->-~&I79{}^rIZjix-&*HxvbQ@sJ3G%avs?Rjqy0_r+b<(p{A&FFF2D94 z77;&+5~9Jft!SXm`aK$W()K8+QCg=#oz!B^3Jq4y^cpm1NV`T!lhRchtX6t;N?Me* zY0##0jRtE~?+PXBly1;qqxx=8a*5JS8f@}+lT4jvTQp`bR_RZomqfn~F2m1$r^PuY7}|Ws8dvv28XIH4UW@_G@9i1 zJ8;InVA0n6!%9}iW9_C!!vo%gj;%h)iz74(6WTlrhso)V?&qn|*fvj?WO-pCOgzm? z9p%{#C-$b{vC$_*?2Mk9B8{!(0BDzs{V29aJEAticJP84d;HojSU7$Z1s=H?&3GTY zMUSyiyrFCCaUkILJ9Sp=Nl>ou!o6ej(oJ`G3o-1CQxZ`pQT z`P@YL%_JUn@y<`(l*h!iu0*a|p}fN*aE@v7Wd0!WONpfOmYhR`Ug)Ae-0iWJbJX>dlVe> zbe22Ke(j2baQZv9$!1I%#!0}e9>+y$B8ggL#$lPbUI!uckr_UM@3vUHZnuI!=+2~a zCUDFu!nQHr!MUB}rtFVYZoNT0-R zx-@b=7s{?IGS26GJl4!-T@iQ58bN(hBI7UE2=y?u@km$WyPqwMSJ~+p=u~Kwm}8Sj zCxUB z16TvI{HrWPKeU{#?!oV6n26ztamQg&8Y$R9HFt@1&$wX4_jk>{oABl>?Bmb}2yP%d zs}gPUDNm5{MvYc&U0w68zk!l}!74vONf3!E0=MgQx{aDw^)NA+QOwh)*%hiY6i00; zc1bD4HMai@%xi*qilZ}Yozb-oR)VfzR{P$ZM%}uco%uhPUFiz3X06)znp0^OolcvS|>Ltb*_Db9{Rp0O@}Y0UA{00C@NE%=p*2AxB1)(>@lo{tYNX=e6t?6yM*0f@R-^ z(_*)0o84zsiy{NGTB^Ya;gtd}H7N)XlOT{gU=Z+?u}tuZ(=`a9d=vye=KHuY_+W_t z6~rwT=lqk_JGr9x00#?N;L%uV@QK)3ZCz', + '', + ''], + migrations) + + def test_app_label(self): + self.assertEqual(['fakeapp', 'fakeapp', 'fakeapp'], + [m.app_label() for m in self.fakeapp]) + + def test_name(self): + self.assertEqual(['0001_spam', '0002_eggs', '0003_alter_spam'], + [m.name() for m in self.fakeapp]) + + def test_full_name(self): + self.assertEqual(['fakeapp.migrations.0001_spam', + 'fakeapp.migrations.0002_eggs', + 'fakeapp.migrations.0003_alter_spam'], + [m.full_name() for m in self.fakeapp]) + + def test_migration(self): + # Can't use vanilla import, modules beginning with numbers aren't in grammar + M1 = __import__("fakeapp.migrations.0001_spam", {}, {}, ['Migration']).Migration + M2 = __import__("fakeapp.migrations.0002_eggs", {}, {}, ['Migration']).Migration + M3 = __import__("fakeapp.migrations.0003_alter_spam", {}, {}, ['Migration']).Migration + self.assertEqual([M1, M2, M3], + [m.migration().Migration for m in self.fakeapp]) + self.assertRaises(exceptions.UnknownMigration, + self.fakeapp['9999_unknown'].migration) + + def test_previous(self): + self.assertEqual([None, + self.fakeapp['0001_spam'], + self.fakeapp['0002_eggs']], + [m.previous() for m in self.fakeapp]) + + def test_dependencies(self): + "Test that the dependency detection works." + self.assertEqual([ + set([]), + set([self.fakeapp['0001_spam']]), + set([self.fakeapp['0002_eggs']]) + ], + [m.dependencies for m in self.fakeapp], + ) + self.assertEqual([ + set([self.fakeapp['0001_spam']]), + set([self.otherfakeapp['0001_first']]), + set([ + self.otherfakeapp['0002_second'], + self.fakeapp['0003_alter_spam'], + ]) + ], + [m.dependencies for m in self.otherfakeapp], + ) + + def test_forwards_plan(self): + self.assertEqual([ + [self.fakeapp['0001_spam']], + [ + self.fakeapp['0001_spam'], + self.fakeapp['0002_eggs'] + ], + [ + self.fakeapp['0001_spam'], + self.fakeapp['0002_eggs'], + self.fakeapp['0003_alter_spam'], + ] + ], + [m.forwards_plan() for m in self.fakeapp], + ) + self.assertEqual([ + [ + self.fakeapp['0001_spam'], + self.otherfakeapp['0001_first'] + ], + [ + self.fakeapp['0001_spam'], + self.otherfakeapp['0001_first'], + self.otherfakeapp['0002_second'] + ], + [ + self.fakeapp['0001_spam'], + self.otherfakeapp['0001_first'], + self.otherfakeapp['0002_second'], + self.fakeapp['0002_eggs'], + self.fakeapp['0003_alter_spam'], + self.otherfakeapp['0003_third'], + ] + ], + [m.forwards_plan() for m in self.otherfakeapp], + ) + + def test_is_before(self): + F1 = self.fakeapp['0001_spam'] + F2 = self.fakeapp['0002_eggs'] + F3 = self.fakeapp['0003_alter_spam'] + O1 = self.otherfakeapp['0001_first'] + O2 = self.otherfakeapp['0002_second'] + O3 = self.otherfakeapp['0003_third'] + self.assertTrue(F1.is_before(F2)) + self.assertTrue(F1.is_before(F3)) + self.assertTrue(F2.is_before(F3)) + self.assertEqual(O3.is_before(O1), False) + self.assertEqual(O3.is_before(O2), False) + self.assertEqual(O2.is_before(O2), False) + self.assertEqual(O2.is_before(O1), False) + self.assertEqual(F2.is_before(O1), None) + self.assertEqual(F2.is_before(O2), None) + self.assertEqual(F2.is_before(O3), None) + + +class TestMigrationDependencies(Monkeypatcher): + installed_apps = ['deps_a', 'deps_b', 'deps_c'] + + def setUp(self): + super(TestMigrationDependencies, self).setUp() + self.deps_a = Migrations('deps_a') + self.deps_b = Migrations('deps_b') + self.deps_c = Migrations('deps_c') + Migrations.calculate_dependencies(force=True) + + def test_dependencies(self): + self.assertEqual( + [ + set([]), + set([self.deps_a['0001_a']]), + set([self.deps_a['0002_a']]), + set([ + self.deps_a['0003_a'], + self.deps_b['0003_b'], + ]), + set([self.deps_a['0004_a']]), + ], + [m.dependencies for m in self.deps_a], + ) + self.assertEqual( + [ + set([]), + set([ + self.deps_b['0001_b'], + self.deps_a['0002_a'] + ]), + set([ + self.deps_b['0002_b'], + self.deps_a['0003_a'] + ]), + set([self.deps_b['0003_b']]), + set([self.deps_b['0004_b']]), + ], + [m.dependencies for m in self.deps_b], + ) + self.assertEqual( + [ + set([]), + set([self.deps_c['0001_c']]), + set([self.deps_c['0002_c']]), + set([self.deps_c['0003_c']]), + set([ + self.deps_c['0004_c'], + self.deps_a['0002_a'] + ]), + ], + [m.dependencies for m in self.deps_c], + ) + + def test_dependents(self): + self.assertEqual([set([self.deps_a['0002_a']]), + set([self.deps_c['0005_c'], + self.deps_b['0002_b'], + self.deps_a['0003_a']]), + set([self.deps_b['0003_b'], + self.deps_a['0004_a']]), + set([self.deps_a['0005_a']]), + set([])], + [m.dependents for m in self.deps_a]) + self.assertEqual([set([self.deps_b['0002_b']]), + set([self.deps_b['0003_b']]), + set([self.deps_b['0004_b'], + self.deps_a['0004_a']]), + set([self.deps_b['0005_b']]), + set([])], + [m.dependents for m in self.deps_b]) + self.assertEqual([set([self.deps_c['0002_c']]), + set([self.deps_c['0003_c']]), + set([self.deps_c['0004_c']]), + set([self.deps_c['0005_c']]), + set([])], + [m.dependents for m in self.deps_c]) + + def test_forwards_plan(self): + self.assertEqual([[self.deps_a['0001_a']], + [self.deps_a['0001_a'], + self.deps_a['0002_a']], + [self.deps_a['0001_a'], + self.deps_a['0002_a'], + self.deps_a['0003_a']], + [self.deps_b['0001_b'], + self.deps_a['0001_a'], + self.deps_a['0002_a'], + self.deps_b['0002_b'], + self.deps_a['0003_a'], + self.deps_b['0003_b'], + self.deps_a['0004_a']], + [self.deps_b['0001_b'], + self.deps_a['0001_a'], + self.deps_a['0002_a'], + self.deps_b['0002_b'], + self.deps_a['0003_a'], + self.deps_b['0003_b'], + self.deps_a['0004_a'], + self.deps_a['0005_a']]], + [m.forwards_plan() for m in self.deps_a]) + self.assertEqual([[self.deps_b['0001_b']], + [self.deps_b['0001_b'], + self.deps_a['0001_a'], + self.deps_a['0002_a'], + self.deps_b['0002_b']], + [self.deps_b['0001_b'], + self.deps_a['0001_a'], + self.deps_a['0002_a'], + self.deps_b['0002_b'], + self.deps_a['0003_a'], + self.deps_b['0003_b']], + [self.deps_b['0001_b'], + self.deps_a['0001_a'], + self.deps_a['0002_a'], + self.deps_b['0002_b'], + self.deps_a['0003_a'], + self.deps_b['0003_b'], + self.deps_b['0004_b']], + [self.deps_b['0001_b'], + self.deps_a['0001_a'], + self.deps_a['0002_a'], + self.deps_b['0002_b'], + self.deps_a['0003_a'], + self.deps_b['0003_b'], + self.deps_b['0004_b'], + self.deps_b['0005_b']]], + [m.forwards_plan() for m in self.deps_b]) + self.assertEqual([[self.deps_c['0001_c']], + [self.deps_c['0001_c'], + self.deps_c['0002_c']], + [self.deps_c['0001_c'], + self.deps_c['0002_c'], + self.deps_c['0003_c']], + [self.deps_c['0001_c'], + self.deps_c['0002_c'], + self.deps_c['0003_c'], + self.deps_c['0004_c']], + [self.deps_c['0001_c'], + self.deps_c['0002_c'], + self.deps_c['0003_c'], + self.deps_c['0004_c'], + self.deps_a['0001_a'], + self.deps_a['0002_a'], + self.deps_c['0005_c']]], + [m.forwards_plan() for m in self.deps_c]) + + def test_backwards_plan(self): + self.assertEqual([ + [ + self.deps_c['0005_c'], + self.deps_b['0005_b'], + self.deps_b['0004_b'], + self.deps_a['0005_a'], + self.deps_a['0004_a'], + self.deps_b['0003_b'], + self.deps_b['0002_b'], + self.deps_a['0003_a'], + self.deps_a['0002_a'], + self.deps_a['0001_a'], + ], + [ + self.deps_c['0005_c'], + self.deps_b['0005_b'], + self.deps_b['0004_b'], + self.deps_a['0005_a'], + self.deps_a['0004_a'], + self.deps_b['0003_b'], + self.deps_b['0002_b'], + self.deps_a['0003_a'], + self.deps_a['0002_a'], + ], + [ + self.deps_b['0005_b'], + self.deps_b['0004_b'], + self.deps_a['0005_a'], + self.deps_a['0004_a'], + self.deps_b['0003_b'], + self.deps_a['0003_a'], + ], + [ + self.deps_a['0005_a'], + self.deps_a['0004_a'], + ], + [ + self.deps_a['0005_a'], + ] + ], [m.backwards_plan() for m in self.deps_a]) + self.assertEqual([ + [ + self.deps_b['0005_b'], + self.deps_b['0004_b'], + self.deps_a['0005_a'], + self.deps_a['0004_a'], + self.deps_b['0003_b'], + self.deps_b['0002_b'], + self.deps_b['0001_b'], + ], + [ + self.deps_b['0005_b'], + self.deps_b['0004_b'], + self.deps_a['0005_a'], + self.deps_a['0004_a'], + self.deps_b['0003_b'], + self.deps_b['0002_b'], + ], + [ + self.deps_b['0005_b'], + self.deps_b['0004_b'], + self.deps_a['0005_a'], + self.deps_a['0004_a'], + self.deps_b['0003_b'], + ], + [ + self.deps_b['0005_b'], + self.deps_b['0004_b'], + ], + [ + self.deps_b['0005_b'], + ], + ], [m.backwards_plan() for m in self.deps_b]) + self.assertEqual([ + [ + self.deps_c['0005_c'], + self.deps_c['0004_c'], + self.deps_c['0003_c'], + self.deps_c['0002_c'], + self.deps_c['0001_c'], + ], + [ + self.deps_c['0005_c'], + self.deps_c['0004_c'], + self.deps_c['0003_c'], + self.deps_c['0002_c'], + ], + [ + self.deps_c['0005_c'], + self.deps_c['0004_c'], + self.deps_c['0003_c'], + ], + [ + self.deps_c['0005_c'], + self.deps_c['0004_c'], + ], + [self.deps_c['0005_c']] + ], [m.backwards_plan() for m in self.deps_c]) + + +class TestCircularDependencies(Monkeypatcher): + installed_apps = ["circular_a", "circular_b"] + + def test_plans(self): + Migrations.calculate_dependencies(force=True) + circular_a = Migrations('circular_a') + circular_b = Migrations('circular_b') + self.assertRaises( + exceptions.CircularDependency, + circular_a[-1].forwards_plan, + ) + self.assertRaises( + exceptions.CircularDependency, + circular_b[-1].forwards_plan, + ) + self.assertRaises( + exceptions.CircularDependency, + circular_a[-1].backwards_plan, + ) + self.assertRaises( + exceptions.CircularDependency, + circular_b[-1].backwards_plan, + ) + + +class TestMigrations(Monkeypatcher): + installed_apps = ["fakeapp", "otherfakeapp"] + + def test_all(self): + + M1 = Migrations(__import__("fakeapp", {}, {}, [''])) + M2 = Migrations(__import__("otherfakeapp", {}, {}, [''])) + + self.assertEqual( + [M1, M2], + list(all_migrations()), + ) + + def test(self): + + M1 = Migrations(__import__("fakeapp", {}, {}, [''])) + + self.assertEqual(M1, Migrations("fakeapp")) + self.assertEqual(M1, Migrations(self.create_fake_app("fakeapp"))) + + def test_application(self): + fakeapp = Migrations("fakeapp") + application = __import__("fakeapp", {}, {}, ['']) + self.assertEqual(application, fakeapp.application) + + def test_migration(self): + # Can't use vanilla import, modules beginning with numbers aren't in grammar + M1 = __import__("fakeapp.migrations.0001_spam", {}, {}, ['Migration']).Migration + M2 = __import__("fakeapp.migrations.0002_eggs", {}, {}, ['Migration']).Migration + migration = Migrations('fakeapp') + self.assertEqual(M1, migration['0001_spam'].migration().Migration) + self.assertEqual(M2, migration['0002_eggs'].migration().Migration) + self.assertRaises(exceptions.UnknownMigration, + migration['0001_jam'].migration) + + def test_guess_migration(self): + # Can't use vanilla import, modules beginning with numbers aren't in grammar + M1 = __import__("fakeapp.migrations.0001_spam", {}, {}, ['Migration']).Migration + migration = Migrations('fakeapp') + self.assertEqual(M1, migration.guess_migration("0001_spam").migration().Migration) + self.assertEqual(M1, migration.guess_migration("0001_spa").migration().Migration) + self.assertEqual(M1, migration.guess_migration("0001_sp").migration().Migration) + self.assertEqual(M1, migration.guess_migration("0001_s").migration().Migration) + self.assertEqual(M1, migration.guess_migration("0001_").migration().Migration) + self.assertEqual(M1, migration.guess_migration("0001").migration().Migration) + self.assertRaises(exceptions.UnknownMigration, + migration.guess_migration, "0001-spam") + self.assertRaises(exceptions.MultiplePrefixMatches, + migration.guess_migration, "000") + self.assertRaises(exceptions.MultiplePrefixMatches, + migration.guess_migration, "") + self.assertRaises(exceptions.UnknownMigration, + migration.guess_migration, "0001_spams") + self.assertRaises(exceptions.UnknownMigration, + migration.guess_migration, "0001_jam") + + def test_app_label(self): + names = ['fakeapp', 'otherfakeapp'] + self.assertEqual(names, + [Migrations(n).app_label() for n in names]) + + def test_full_name(self): + names = ['fakeapp', 'otherfakeapp'] + self.assertEqual([n + '.migrations' for n in names], + [Migrations(n).full_name() for n in names]) + + +class TestMigrationLogic(Monkeypatcher): + + """ + Tests if the various logic functions in migration actually work. + """ + + installed_apps = ["fakeapp", "otherfakeapp"] + + def assertListEqual(self, list1, list2, msg=None): + list1 = list(list1) + list2 = list(list2) + list1.sort() + list2.sort() + return self.assert_(list1 == list2, "%s is not equal to %s" % (list1, list2)) + + def test_find_ghost_migrations(self): + pass + + def test_apply_migrations(self): + MigrationHistory.objects.all().delete() + migrations = Migrations("fakeapp") + + # We should start with no migrations + self.assertEqual(list(MigrationHistory.objects.all()), []) + + # Apply them normally + migrate_app(migrations, target_name=None, fake=False, + load_initial_data=True) + + # We should finish with all migrations + self.assertListEqual( + ((u"fakeapp", u"0001_spam"), + (u"fakeapp", u"0002_eggs"), + (u"fakeapp", u"0003_alter_spam"),), + MigrationHistory.objects.values_list("app_name", "migration"), + ) + + # Now roll them backwards + migrate_app(migrations, target_name="zero", fake=False) + + # Finish with none + self.assertEqual(list(MigrationHistory.objects.all()), []) + + + def test_migration_merge_forwards(self): + MigrationHistory.objects.all().delete() + migrations = Migrations("fakeapp") + + # We should start with no migrations + self.assertEqual(list(MigrationHistory.objects.all()), []) + + # Insert one in the wrong order + MigrationHistory.objects.create(app_name = "fakeapp", + migration = "0002_eggs", + applied = datetime.datetime.now()) + + # Did it go in? + self.assertListEqual( + ((u"fakeapp", u"0002_eggs"),), + MigrationHistory.objects.values_list("app_name", "migration"), + ) + + # Apply them normally + self.assertRaises(exceptions.InconsistentMigrationHistory, + migrate_app, + migrations, target_name=None, fake=False) + self.assertRaises(exceptions.InconsistentMigrationHistory, + migrate_app, + migrations, target_name='zero', fake=False) + try: + migrate_app(migrations, target_name=None, fake=False) + except exceptions.InconsistentMigrationHistory, e: + self.assertEqual( + [ + ( + migrations['0002_eggs'], + migrations['0001_spam'], + ) + ], + e.problems, + ) + try: + migrate_app(migrations, target_name="zero", fake=False) + except exceptions.InconsistentMigrationHistory, e: + self.assertEqual( + [ + ( + migrations['0002_eggs'], + migrations['0001_spam'], + ) + ], + e.problems, + ) + + # Nothing should have changed (no merge mode!) + self.assertListEqual( + ((u"fakeapp", u"0002_eggs"),), + MigrationHistory.objects.values_list("app_name", "migration"), + ) + + # Apply with merge + migrate_app(migrations, target_name=None, merge=True, fake=False) + + # We should finish with all migrations + self.assertListEqual( + ((u"fakeapp", u"0001_spam"), + (u"fakeapp", u"0002_eggs"), + (u"fakeapp", u"0003_alter_spam"),), + MigrationHistory.objects.values_list("app_name", "migration"), + ) + + # Now roll them backwards + migrate_app(migrations, target_name="0002", fake=False) + migrate_app(migrations, target_name="0001", fake=True) + migrate_app(migrations, target_name="zero", fake=False) + + # Finish with none + self.assertEqual(list(MigrationHistory.objects.all()), []) + + def test_alter_column_null(self): + + def null_ok(eat_exception=True): + from django.db import connection, transaction + # the DBAPI introspection module fails on postgres NULLs. + cursor = connection.cursor() + + # SQLite has weird now() + if db.backend_name == "sqlite3": + now_func = "DATETIME('NOW')" + # So does SQLServer... should we be using a backend attribute? + elif db.backend_name == "pyodbc": + now_func = "GETDATE()" + elif db.backend_name == "oracle": + now_func = "SYSDATE" + else: + now_func = "NOW()" + + try: + if db.backend_name == "pyodbc": + cursor.execute("SET IDENTITY_INSERT southtest_spam ON;") + cursor.execute("INSERT INTO southtest_spam (id, weight, expires, name) VALUES (100, NULL, %s, 'whatever');" % now_func) + except: + if eat_exception: + transaction.rollback() + return False + else: + raise + else: + cursor.execute("DELETE FROM southtest_spam") + transaction.commit() + return True + + MigrationHistory.objects.all().delete() + migrations = Migrations("fakeapp") + + # by default name is NOT NULL + migrate_app(migrations, target_name="0002", fake=False) + self.failIf(null_ok()) + self.assertListEqual( + ((u"fakeapp", u"0001_spam"), + (u"fakeapp", u"0002_eggs"),), + MigrationHistory.objects.values_list("app_name", "migration"), + ) + + # after 0003, it should be NULL + migrate_app(migrations, target_name="0003", fake=False) + self.assert_(null_ok(False)) + self.assertListEqual( + ((u"fakeapp", u"0001_spam"), + (u"fakeapp", u"0002_eggs"), + (u"fakeapp", u"0003_alter_spam"),), + MigrationHistory.objects.values_list("app_name", "migration"), + ) + + # make sure it is NOT NULL again + migrate_app(migrations, target_name="0002", fake=False) + self.failIf(null_ok(), 'weight not null after migration') + self.assertListEqual( + ((u"fakeapp", u"0001_spam"), + (u"fakeapp", u"0002_eggs"),), + MigrationHistory.objects.values_list("app_name", "migration"), + ) + + # finish with no migrations, otherwise other tests fail... + migrate_app(migrations, target_name="zero", fake=False) + self.assertEqual(list(MigrationHistory.objects.all()), []) + + def test_dependencies(self): + + fakeapp = Migrations("fakeapp") + otherfakeapp = Migrations("otherfakeapp") + + # Test a simple path + self.assertEqual([fakeapp['0001_spam'], + fakeapp['0002_eggs'], + fakeapp['0003_alter_spam']], + fakeapp['0003_alter_spam'].forwards_plan()) + + # And a complex one. + self.assertEqual( + [ + fakeapp['0001_spam'], + otherfakeapp['0001_first'], + otherfakeapp['0002_second'], + fakeapp['0002_eggs'], + fakeapp['0003_alter_spam'], + otherfakeapp['0003_third'] + ], + otherfakeapp['0003_third'].forwards_plan(), + ) + + +class TestMigrationUtils(Monkeypatcher): + installed_apps = ["fakeapp", "otherfakeapp"] + + def test_get_app_label(self): + self.assertEqual( + "southtest", + get_app_label(self.create_fake_app("southtest.models")), + ) + self.assertEqual( + "baz", + get_app_label(self.create_fake_app("foo.bar.baz.models")), + ) + +class TestUtils(unittest.TestCase): + + def test_flatten(self): + self.assertEqual([], list(flatten(iter([])))) + self.assertEqual([], list(flatten(iter([iter([]), ])))) + self.assertEqual([1], list(flatten(iter([1])))) + self.assertEqual([1, 2], list(flatten(iter([1, 2])))) + self.assertEqual([1, 2], list(flatten(iter([iter([1]), 2])))) + self.assertEqual([1, 2], list(flatten(iter([iter([1, 2])])))) + self.assertEqual([1, 2, 3], list(flatten(iter([iter([1, 2]), 3])))) + self.assertEqual([1, 2, 3], + list(flatten(iter([iter([1]), iter([2]), 3])))) + self.assertEqual([1, 2, 3], + list(flatten([[1], [2], 3]))) + + def test_depends(self): + graph = {'A1': []} + self.assertEqual(['A1'], + depends('A1', lambda n: graph[n])) + graph = {'A1': [], + 'A2': ['A1'], + 'A3': ['A2']} + self.assertEqual(['A1', 'A2', 'A3'], + depends('A3', lambda n: graph[n])) + graph = {'A1': [], + 'A2': ['A1'], + 'A3': ['A2', 'A1']} + self.assertEqual(['A1', 'A2', 'A3'], + depends('A3', lambda n: graph[n])) + graph = {'A1': [], + 'A2': ['A1'], + 'A3': ['A2', 'A1', 'B1'], + 'B1': []} + self.assertEqual( + ['B1', 'A1', 'A2', 'A3'], + depends('A3', lambda n: graph[n]), + ) + graph = {'A1': [], + 'A2': ['A1'], + 'A3': ['A2', 'A1', 'B2'], + 'B1': [], + 'B2': ['B1']} + self.assertEqual( + ['B1', 'B2', 'A1', 'A2', 'A3'], + depends('A3', lambda n: graph[n]), + ) + graph = {'A1': [], + 'A2': ['A1', 'B1'], + 'A3': ['A2'], + 'B1': ['A1']} + self.assertEqual(['A1', 'B1', 'A2', 'A3'], + depends('A3', lambda n: graph[n])) + graph = {'A1': [], + 'A2': ['A1'], + 'A3': ['A2', 'A1', 'B2'], + 'B1': [], + 'B2': ['B1', 'C1'], + 'C1': ['B1']} + self.assertEqual( + ['B1', 'C1', 'B2', 'A1', 'A2', 'A3'], + depends('A3', lambda n: graph[n]), + ) + graph = {'A1': [], + 'A2': ['A1'], + 'A3': ['A2', 'B2', 'A1', 'C1'], + 'B1': ['A1'], + 'B2': ['B1', 'C2', 'A1'], + 'C1': ['B1'], + 'C2': ['C1', 'A1'], + 'C3': ['C2']} + self.assertEqual( + ['A1', 'B1', 'C1', 'C2', 'B2', 'A2', 'A3'], + depends('A3', lambda n: graph[n]), + ) + + def assertCircularDependency(self, trace, target, graph): + "Custom assertion that checks a circular dependency is detected correctly." + self.assertRaises( + exceptions.CircularDependency, + depends, + target, + lambda n: graph[n], + ) + try: + depends(target, lambda n: graph[n]) + except exceptions.CircularDependency, e: + self.assertEqual(trace, e.trace) + + def test_depends_cycle(self): + graph = {'A1': ['A1']} + self.assertCircularDependency( + ['A1', 'A1'], + 'A1', + graph, + ) + graph = {'A1': [], + 'A2': ['A1', 'A2'], + 'A3': ['A2']} + self.assertCircularDependency( + ['A2', 'A2'], + 'A3', + graph, + ) + graph = {'A1': [], + 'A2': ['A1'], + 'A3': ['A2', 'A3'], + 'A4': ['A3']} + self.assertCircularDependency( + ['A3', 'A3'], + 'A4', + graph, + ) + graph = {'A1': ['B1'], + 'B1': ['A1']} + self.assertCircularDependency( + ['A1', 'B1', 'A1'], + 'A1', + graph, + ) + graph = {'A1': [], + 'A2': ['A1', 'B2'], + 'A3': ['A2'], + 'B1': [], + 'B2': ['B1', 'A2'], + 'B3': ['B2']} + self.assertCircularDependency( + ['A2', 'B2', 'A2'], + 'A3', + graph, + ) + graph = {'A1': [], + 'A2': ['A1', 'B3'], + 'A3': ['A2'], + 'B1': [], + 'B2': ['B1', 'A2'], + 'B3': ['B2']} + self.assertCircularDependency( + ['A2', 'B3', 'B2', 'A2'], + 'A3', + graph, + ) + graph = {'A1': [], + 'A2': ['A1'], + 'A3': ['A2', 'B2'], + 'A4': ['A3'], + 'B1': ['A3'], + 'B2': ['B1']} + self.assertCircularDependency( + ['A3', 'B2', 'B1', 'A3'], + 'A4', + graph, + ) + +class TestManualChanges(Monkeypatcher): + installed_apps = ["fakeapp", "otherfakeapp"] + + def test_suggest_name(self): + migrations = Migrations('fakeapp') + change = ManualChanges(migrations, + [], + ['fakeapp.slug'], + []) + self.assertEquals(change.suggest_name(), + 'add_field_fakeapp_slug') + + change = ManualChanges(migrations, + [], + [], + ['fakeapp.slug']) + self.assertEquals(change.suggest_name(), + 'add_index_fakeapp_slug') diff --git a/lib/python/south/tests/logic.pyc b/lib/python/south/tests/logic.pyc new file mode 100644 index 0000000000000000000000000000000000000000..115c4a97979d0e56dad7e647fd9867ee153bf49b GIT binary patch literal 25670 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 lEf?Ipoxxd<8Se-l{0g%V%>K;m|IN{~yqw&vF0& literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/non_managed/__init__.py b/lib/python/south/tests/non_managed/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/python/south/tests/non_managed/__init__.pyc b/lib/python/south/tests/non_managed/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dd02a882d02da44f02035a912313800703bf3753 GIT binary patch literal 165 zcmZSn%*$mSA03d)00oRd+5w1*S%5?e14FO|NW@PANHCxg#eP6B{m|mnqGJ7|qQtzE z{5<{O%#ze%{gTp()RcVPoXjMB5H6@J$pFd~=a-gb=mX_TiuLpI^Wt+8^AgijQ}pBG eGxIV_;^Xyz>NtQ#+T`Y^l;)(`fh;ZtVg>-8=_j`U literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/non_managed/migrations/__init__.py b/lib/python/south/tests/non_managed/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/python/south/tests/non_managed/migrations/__init__.pyc b/lib/python/south/tests/non_managed/migrations/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..65d37725925b07789bc0c9c5afdcd7a3f0fb71e0 GIT binary patch literal 176 zcmZ9GK?(vf3`Hxt5W#!QMzeGy;t2*}a3L;A$CjDEG?69)dTuXZ3a$*iANZeqvixi} z2YC~OeUqu42@I-|v<9|f9# z)^(}A@drAhy$L1n!*LSm=k3DMd1~w?ajmA({-m+a2o4!If`pi!n@(+~zjVE0YA-1e zy(XJQkVX=C3mFMuSxZ*atXMLMBdx#m9YMWWFBz$1By}SP<2vjuFL6QP!l3#M$ebeJ z2_*@TpvXT{06|f&$oiP7L!>HEwS!3r59qCwPVJ185l%{yb{KfcHQlD&$b8P!5DXqs zHaohe6z)Sfr)Bjym@!oCsGRn$+E{YD!o!Wx{@GyLisyd9o@#;ZulPnn<-C(~*D9HN zx}WNT8a}2OSoJDfWeJR#)y$~oyQpabKBVxzSL2yAgN`n-DrDIo9GcGjQ3>{&iM{W+ z$L!w=VC$J#V4|!1X?C1{OwO~7Qfj{?r+J6IQm*r-x=nIe-35QiHMjWRJmX!zN;m&H K{#eQIC;1&l^x(1p literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/otherfakeapp/__init__.py b/lib/python/south/tests/otherfakeapp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/python/south/tests/otherfakeapp/__init__.pyc b/lib/python/south/tests/otherfakeapp/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c4c80f9152bf659ec11cc1dde9f7d93f4d8c02fd GIT binary patch literal 166 zcmZSn%*$mSA03d)00oRd+5w1*S%5?e14FO|NW@PANHCxg#r{As{m|mnqGJ7|qQtzE z{5<{O%#ze%{gTp()RcVPoXjMB5H6@J$pFd~=a-gb=mX_TiuLnLGE$4u60=hi3kvk( f<1_OzOXB18fC@Q)X4>TDr#EfNz8 zwc^X&*}m_)pWt&mn)AoICG;!6|0QlSMogSfHX{kzMhp65Zt5(=T*LwnV`3ko=L7{+fF=px$ zGQuD%qLLCqvWO|1CAeH{lxP|kbT*rvanp!uN$|UUjk*a@NI;6@y^T>o$BPb(D)Y`o ze)EKa-5b>!mbIdmx@L>QD#PsdRY`qX78#@6yxBFL8NIb@hUeBWZP%*pDY0soqhXF& zUbMze@zpT`2K}h)b8yX` k$}GVf;D$~2cUcK0q_p+ zko7?KA&rmFxBS8rGn9Npq4G{1pj=yRD(8nN-j>T&`O-E&0LbGS$(mi_S!L_k>K$ooSuOTh(aWh_v*^3BP?c z+MZQqE{Ha7eua0=Zv9H&y?4U;m1!4hZPaEX>U|APOsCUx=}ci8Jwdl)%&@TSi)yu_ z!X1GI_IKC`jpLw@X(+iW)&VmqJIpaPFcc(ZqiQ3iAE8A`9Okw{HK7MdAK5S7ApL)z z6KZQ1FtI^2uU`(jqaj zP%FONo$dR+`w2eBqZxm^TR^`8{9ocWGZYd(4@!W_gVY0$Y#&M=ssPde?Eqv1GK3F+ zcYud91UZ0te1LC@FI=&Qn)NAE*69)Im6obBc8Katv1)}abiE@$m#=V}V-yMq7h|SA zAtMaJA}T2%B=eZUS%O=NwGvHZk5HdZr)ON0R<+zTqFNA%?Y>6cgeW8+QS#o#D4=6U z2S%04&P9Ingo520)f$$yqL#X5^TH~_?B-QTeNq+~quspO9M6p2*qq_HHB8%FwLLLb zZ8J5@@w0i+%2RxGhyY97zKB+u5g}3q!23JvWPtbg$(S0KiN)Gc9;4Ayj){3f6O>RP zT}glYh^tGP^IBAjb4xjMjx(_-x29B$s-@w&PN=~NHTWk%r~P|h5=zF+15t>AC>RF) qsO)lZ%|4Y`f;Yg;vwRlyN|TF5%f;9?aaY~*uAUdf!j1dHoA}?|E9Nl( literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/otherfakeapp/migrations/__init__.py b/lib/python/south/tests/otherfakeapp/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/python/south/tests/otherfakeapp/migrations/__init__.pyc b/lib/python/south/tests/otherfakeapp/migrations/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8fe744b8f36f6794bf75afda33bc8c3caac022ba GIT binary patch literal 177 zcmZ9GK?(vf3`Hxt5W#!QMzeGy;t50;+~}foY;?-ZG^I&F&+P?F!Igpc1OJmx=AZRy zXK&}b`OQrCQQ?J(H>Pc>b81w~pYT;ZxG-jhF1zR{LK_4bq`c76W(*xv(p!4Xn2PkE oxiFCQm>+IHml90jnVkfR4AzDi#M-UiEVX_=kuM`2>JzM(FXWmmvH$=8 literal 0 HcmV?d00001 diff --git a/lib/python/south/tests/otherfakeapp/models.py b/lib/python/south/tests/otherfakeapp/models.py new file mode 100644 index 0000000..93a4b8e --- /dev/null +++ b/lib/python/south/tests/otherfakeapp/models.py @@ -0,0 +1 @@ +# This file left intentionally blank. \ No newline at end of file diff --git a/lib/python/south/tests/otherfakeapp/models.pyc b/lib/python/south/tests/otherfakeapp/models.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ac5a742015a02de0d0f7ebec70b7cd48f07127c1 GIT binary patch literal 164 zcmZSn%*$mSA03d)00oRd+5w1*S%5?e14FO|NW@PANHCxg#lAo>{m|mnqGJ7|qQtzE z{5<{O%#ze%{gTp()RcVPoXjMB5H6@J$pFd~=a-gb=mX_TiuLnLGE$4u60=hi3kvje c^HWlDiuHiXIDjVF0J)_(sdgZHi-DK{0D8bDjQ{`u literal 0 HcmV?d00001 diff --git a/lib/python/south/utils/__init__.py b/lib/python/south/utils/__init__.py new file mode 100644 index 0000000..c3c5191 --- /dev/null +++ b/lib/python/south/utils/__init__.py @@ -0,0 +1,73 @@ +""" +Generally helpful utility functions. +""" + + +def _ask_for_it_by_name(name): + "Returns an object referenced by absolute path." + bits = name.split(".") + + ## what if there is no absolute reference? + if len(bits)>1: + modulename = ".".join(bits[:-1]) + else: + modulename=bits[0] + + module = __import__(modulename, {}, {}, bits[-1]) + + if len(bits) == 1: + return module + else: + return getattr(module, bits[-1]) + + +def ask_for_it_by_name(name): + "Returns an object referenced by absolute path. (Memoised outer wrapper)" + if name not in ask_for_it_by_name.cache: + ask_for_it_by_name.cache[name] = _ask_for_it_by_name(name) + return ask_for_it_by_name.cache[name] +ask_for_it_by_name.cache = {} + + +def get_attribute(item, attribute): + """ + Like getattr, but recursive (i.e. you can ask for 'foo.bar.yay'.) + """ + value = item + for part in attribute.split("."): + value = getattr(value, part) + return value + +def auto_through(field): + "Returns if the M2M class passed in has an autogenerated through table or not." + return ( + # Django 1.0/1.1 + (not field.rel.through) + or + # Django 1.2+ + getattr(getattr(field.rel.through, "_meta", None), "auto_created", False) + ) + +def auto_model(model): + "Returns if the given model was automatically generated." + return getattr(model._meta, "auto_created", False) + +def memoize(function): + "Standard memoization decorator." + name = function.__name__ + _name = '_' + name + + def method(self): + if not hasattr(self, _name): + value = function(self) + setattr(self, _name, value) + return getattr(self, _name) + + def invalidate(): + if hasattr(method, _name): + delattr(method, _name) + + method.__name__ = function.__name__ + method.__doc__ = function.__doc__ + method._invalidate = invalidate + return method diff --git a/lib/python/south/utils/__init__.pyc b/lib/python/south/utils/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eac23ea45c22170b7787ff3a0f2531c69c1d7d2f GIT binary patch literal 2889 zcmb_eZI9bT5FWo=?h>vPuHwoGp)PN2DuILG!+oGas30l?cS4R35-+kGdvo!fV~5>! z+o0>G1KR#xegHf(n}oMdNU`F{?0P*r^UO1|j()#3IL^NRx}en+;{Oxe_6vxT=sU=O zet{BD8Hy58MWjQ@A}S-9iK&c5*`abrlpd8mQFf`^rSWi&P2PqWJkX6cd0j8nOxKHP zSF6rdb>)_7+BJo%+Que>BEZrZcMtccxb3eHmqh2{&Q(B90&)Re1_T{`UBqOL+0*lo zC*hakMIki<8OL;lfs5!;yhi|j)zMWg;PdPwb4zXOEp=ld}72oA$RcoZH4aaeHld~=wR*7ul+sYz4h({as$mENKHg|{Yjyr((Z!D6VA}?k- z%mNFetbjInR6xU>`yO3Lx|2+f2<(L45Y(D{CeLz=*Cw zUi8x;eGAHuhiu4x2{BN(zN#*?TJh*zHR+hTMQ3dFP^)2;=tM2sP8DD_5K+jkx-)It zWRja?nJ@1ocg0Eyb0n5iPD#o%{)?61)7&I*RTZc0^>^ zW>_U9P~8wQ|K$`C|Brrn2%W*OPuxuFRn>g zO_iHzb@IW9D(c)?l)YuHRgIeE(k1fFwP(`)9Om83wB6ZExqMP<1&Ev0B?3`~vd!H- zZ1WpgVRVicR=t9`YIH4~U}Zvpmj-a!HXh~A^V(`5t5}>?x-Pem^~S}OBX{AOWvWtq zE!x1m;V!oe6g49aKvCeKhf=tCjEDb%@(grr6q=W=)gy)wXv}k06>@xRkW5lGEJZ9x zq+H4AO`c;~3IJkc`v7oDkQ|A9u1(9qknePyDjxE`{Q=^ca12Kmf=BJ%d>+zMqNgEE zL#EbmM9%|yil3i}cm_r6Uwal%=1l~|I!xdZ6p<@J-oh?phmM|`Qq8&D{S3g`MwPl~ zP3~Ib528%6!_IMb7(e?FgwGC)qI=+s!^TJ!TJh?%#rFQpjVR!rES=h~_FehZw^BZ( zzf}mTwTesgTv`dYWFxNLq;7gSP$z@MXYf$q=)&t@DZt9fCn(}5t>{4nXnl$042a4GRIDR7kr&x;o!koIijJi8swYM(l>a> zbK44t`Byb)sa1&EBN?+^O%pDdA(= (1, 4) and getattr(settings, 'USE_TZ', False): + from django.utils import timezone + from datetime import datetime as _datetime + + class datetime(_datetime): + """ + A custom datetime.datetime class which acts as a compatibility + layer between South and Django 1.4's timezone aware datetime + instances. + + It basically adds the default timezone (as configured in Django's + settings) automatically if no tzinfo is given. + """ + def __new__(cls, year, month, day, + hour=0, minute=0, second=0, microsecond=0, tzinfo=None): + + dt = _datetime(year, month, day, + hour, minute, second, microsecond, + tzinfo=tzinfo) + if tzinfo is None: + default_timezone = timezone.get_default_timezone() + dt = timezone.make_aware(dt, default_timezone) + return dt diff --git a/lib/python/south/utils/datetime_utils.pyc b/lib/python/south/utils/datetime_utils.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a0d586560f92f3d1d49d013124121adcd76455fa GIT binary patch literal 1495 zcmcIk&5qkP5FSbXkCQHtqM} z?H>BReSmg`^3Mx2k~kWYGsADb87cYa?&v4^`Hvj-SC`%|Y52cMXnG%$mF1iyineY>y6DfLgna%qDSOad8N=UZDDbH3{$ABsm0RZwJ}c2 zs|Lx(Y4PSlxpgI;PhLFs;*gn8*Qz$h6OTcx^Qc^I{A6@odxv7FyvY5t7i;^Mag(E) zCCVTllDI=-MB`T){+t8?>>}93uuEXqfn67NJ=pb`{oMd=`q8%`D;nT(0QH}MFCl@jp!A%TgNv%ZFdCDy^3!TwoO1a~a0mwsXkn1?39?25kdq84sQ0`fg2cD79 zmg>^TKzPatAbaG64|LOiE|2rMyq7zefU+ar8qpjK*|7jAku&`~{MsDvgwZ6Di9|Hw91I zAiBz>%vjHu%oF1OA9nbNxiFrK*cV*3|3%?l#{PArz zfA@YC5}_tgh;2!KQw4SWm@M{gfw#~w85i?==7`Mr&n?HlFjCJrM)5d0iyuX2B>w~L C!CYx15S`sk(h^j@4n14qz#>5+IKTmfpdcX*plSmL0+O{^&n9j+Hd@cNL~twr zm>&Q$PLtkxX;<>IJs!_IznRVc?hT*oZ-*6}e;LoOd4%r_24KpVfJvcAV41=+g~Vil z8Psg;!5@G#fUnaYm_A@HS_fNeAI#3iy1TUwz}$dIK9nrK7=~{SKd2LIgZ0k3xvFi0 zFVrcjs&tC5Y1HYWBz0_^QNC77J8w&3-=Jv0{y~*34ri;iSJjk34sfNcTVQKR)vaHu z+}6r@hmVw{2Yf}HJb5@8+JF4z36r3E3}rahwQsd0JzHs4E|K=wd{74bxXHRzN^TwS z2zMEfm103a9?2RNlNhv0`DioVimyDH0DdQMmaqmABiv_rk0hjOi;YqImU0$+(-Eo` zYi!X~IEu~%`$8@usmBnosYznSi;;28JBSdT@oM}fu(IP>TRP+2cwz~|n7W^6{Nu*X z#xj1aXu)T}ceEIvJRR{~GR*5(B3VM3XN