Commits

Andrew Godwin  committed adb43a4 Merge

Merge

  • Participants
  • Parent commits e7067d3, 62b0c48

Comments (0)

Files changed (34)

File docs/conf.py

 master_doc = 'index'
 
 # General information about the project.
-project = u'South'
-copyright = u'2010, Andrew Godwin'
+project = 'South'
+copyright = '2010, Andrew Godwin'
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title, author, documentclass [howto/manual]).
 latex_documents = [
-  ('index', 'South.tex', u'South Documentation',
-   u'Andrew Godwin', 'manual'),
+  ('index', 'South.tex', 'South Documentation',
+   'Andrew Godwin', 'manual'),
 ]
 
 # The name of an image file (relative to this directory) to place at the top of

File south/creator/actions.py

 blocks into the forwards() and backwards() methods, in the right place.
 """
 
+from __future__ import print_function
+
 import sys
 
 from django.db.models.fields.related import RECURSIVE_RELATIONSHIP_CONSTANT
 from south.modelsinspector import value_clean
 from south.creator.freezer import remove_useless_attributes, model_key
 from south.utils import datetime_utils
+from south.utils.py3 import raw_input
 
 
 class Action(object):
             field_def[2]['default'] = repr("")
             return
         # Oh dear. Ask them what to do.
-        print " ? The field '%s.%s' does not have a default specified, yet is NOT NULL." % (
+        print(" ? The field '%s.%s' does not have a default specified, yet is NOT NULL." % (
             self.model._meta.object_name,
             field.name,
-        )
-        print " ? Since you are %s, you MUST specify a default" % self.null_reason
-        print " ? value to use for existing rows. Would you like to:"
-        print " ?  1. Quit now, and add a default to the field in models.py"
-        print " ?  2. Specify a one-off value to use for existing columns now"
+        ))
+        print(" ? Since you are %s, you MUST specify a default" % self.null_reason)
+        print(" ? value to use for existing rows. Would you like to:")
+        print(" ?  1. Quit now, and add a default to the field in models.py")
+        print(" ?  2. Specify a one-off value to use for existing columns now")
         if self.allow_third_null_option:
-            print " ?  3. Disable the backwards migration by raising an exception."
+            print(" ?  3. Disable the backwards migration by raising an exception.")
         while True:
             choice = raw_input(" ? Please select a choice: ")
             if choice == "1":
             elif choice == "3" and self.allow_third_null_option:
                 break
             else:
-                print " ! Invalid choice."
+                print(" ! Invalid choice.")
         if choice == "2":
             self.add_one_time_default(field, field_def)
         elif choice == "3":
 
     def add_one_time_default(self, field, field_def):
         # OK, they want to pick their own one-time default. Who are we to refuse?
-        print " ? Please enter Python code for your one-off default value."
-        print " ? The datetime module is available, so you can do e.g. datetime.date.today()"
+        print(" ? Please enter Python code for your one-off default value.")
+        print(" ? The datetime module is available, so you can do e.g. datetime.date.today()")
         while True:
             code = raw_input(" >>> ")
             if not code:
-                print " ! Please enter some code, or 'exit' (with no quotes) to exit."
+                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
+                except (SyntaxError, NameError) as e:
+                    print(" ! Invalid input: %s" % e)
                 else:
                     break
         # Right, add the default in.

File south/creator/changes.py

 commandline, or by using autodetection, etc.
 """
 
+from __future__ import print_function
+
 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
+from south.utils.py3 import string_types
 
 class BaseChanges(object):
     """
                     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):
+                        if isinstance(unique_together[0], string_types):
                             unique_together = [unique_together]
                         # For each combination, make an action for it
                         for fields in unique_together:
                     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):
+                        if isinstance(unique_together[0], string_types):
                             unique_together = [unique_together]
                         # For each combination, make an action for it
                         for fields in unique_together:
                 # 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):
+                if old_unique_together and isinstance(old_unique_together[0], string_types):
                     old_unique_together = [old_unique_together]
-                if new_unique_together and isinstance(new_unique_together[0], basestring):
+                if new_unique_together and isinstance(new_unique_together[0], string_types):
                     new_unique_together = [new_unique_together]
-                old_unique_together = map(set, old_unique_together)
-                new_unique_together = map(set, new_unique_together)
+                old_unique_together = list(map(set, old_unique_together))
+                new_unique_together = list(map(set, new_unique_together))
                 # See if any appeared or disappeared
                 for item in old_unique_together:
                     if item not in new_unique_together:
     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[0], string_types) and \
             isinstance(triple[1], (list, tuple)) and \
             isinstance(triple[2], dict)
 
             try:
                 model_name, field_name = field_desc.split(".")
             except (TypeError, ValueError):
-                print "%r is not a valid field description." % field_desc
+                print("%r is not a valid field description." % field_desc)
             model = models.get_model(self.migrations.app_label(), model_name)
             yield ("AddIndex", {
                 "model": model,
                 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):
+                    if isinstance(unique_together[0], string_types):
                         unique_together = [unique_together]
                     # For each combination, make an action for it
                     for fields in unique_together:

File south/creator/freezer.py

 Handles freezing of models into FakeORMs.
 """
 
+from __future__ import print_function
+
 import sys
 
 from django.db import models
 
 from south.utils import get_attribute, auto_through
 from south import modelsinspector
+from south.utils.py3 import string_types
 
 def freeze_apps(apps):
     """
     Takes a list of app labels, and returns a string of their frozen form.
     """
-    if isinstance(apps, basestring):
+    if isinstance(apps, string_types):
         apps = [apps]
     frozen_models = set()
     # For each app, add in all its models
                 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__)
+                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"
+        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
     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():
+    for attrname, options in arg_defs + list(kwarg_defs.values()):
         if options.get("ignore_if_auto_through", False) and auto_through(field):
             continue
         if options.get("is_value", False):

File south/db/firebird.py

 # firebird
 
+from __future__ import print_function
+
 import datetime
 
 from django.db import connection, models
 from django.db.utils import DatabaseError
 
 from south.db import generic
+from south.utils.py3 import string_types
 
 class DatabaseOperations(generic.DatabaseOperations):
     backend_name = 'firebird'
                         if callable(default):
                             default = default()
                         # Now do some very cheap quoting. TODO: Redesign return values to avoid this.
-                        if isinstance(default, basestring):
+                        if isinstance(default, string_types):
                             default = "'%s'" % default.replace("'", "''")
                         elif isinstance(default, (datetime.date, datetime.time, datetime.datetime)):
                             default = "'%s'" % default
                         elif isinstance(default, bool):
                             default = int(default)
                         # Escape any % signs in the output (bug #317)
-                        if isinstance(default, basestring):
+                        if isinstance(default, string_types):
                             default = default.replace("%", "%%")
                         # Add it in
                         sql += " DEFAULT %s"
 
         if self.dry_run:
             if self.debug:
-                print '   - no dry run output for alter_column() due to dynamic DDL, sorry'
+                print('   - no dry run output for alter_column() due to dynamic DDL, sorry')
             return
 
 
 
         # Finally, actually change the column
         if self.allows_combined_alters:
-            sqls, values = zip(*sqls)
+            sqls, values = list(zip(*sqls))
             self.execute(
                 "ALTER TABLE %s %s;" % (self.quote_name(table_name), ", ".join(sqls)),
                 generic.flatten(values),
             for sql, values in sqls:
                 try:
                     self.execute("ALTER TABLE %s %s;" % (self.quote_name(table_name), sql), values)
-                except DatabaseError, e:
-                    print e
+                except DatabaseError as e:
+                    print(e)
 
 
         # Execute extra sql, which don't need ALTER TABLE statement

File south/db/generic.py

+from __future__ import print_function
+
 import re
 import sys
 
             return res
 
 from south.logger import get_logger
+from south.utils.py3 import string_types
 
 
 def alias(attrname):
             if table is INVALID:
                 raise INVALID
             elif column_name is None:
-                return table.items()
+                return list(table.items())
             else:
                 return table[column_name]
 
         
         cursor = self._get_connection().cursor()
         if self.debug:
-            print "   = %s" % sql, params
+            print("   = %s" % sql, params)
 
         if self.dry_run:
             return []
 
         try:
             cursor.execute(sql, params)
-        except DatabaseError, e:
-            print >> sys.stderr, 'FATAL ERROR - The following SQL query failed: %s' % sql
-            print >> sys.stderr, 'The error was: %s' % e
+        except DatabaseError as e:
+            print('FATAL ERROR - The following SQL query failed: %s' % sql, file=sys.stderr)
+            print('The error was: %s' % e, file=sys.stderr)
             raise
 
         try:
         """
 
         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."
+            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:
         
         if self.dry_run:
             if self.debug:
-                print '   - no dry run output for alter_column() due to dynamic DDL, sorry'
+                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
         # 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'
+                print('   - no dry run output for delete_unique_column() due to dynamic DDL, sorry')
             return
 
         constraints = list(self._constraints_affecting_columns(table_name, columns))
                         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):
+                        if isinstance(default, string_types):
                             default = "'%s'" % default.replace("'", "''")
                         # Escape any % signs in the output (bug #317)
-                        if isinstance(default, basestring):
+                        if isinstance(default, string_types):
                             default = default.replace("%", "%%")
                         # Add it in
                         sql += " DEFAULT %s"
         """
         if self.dry_run:
             if self.debug:
-                print '   - no dry run output for delete_foreign_key() due to dynamic DDL, sorry'
+                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:
         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"
+            print("No column names supplied on which to create an index")
             return ''
 
         connection = self._get_connection()
         This is possible using only columns due to the deterministic
         index naming function which relies on column names.
         """
-        if isinstance(column_names, (str, unicode)):
+        if isinstance(column_names, string_types):
             column_names = [column_names]
         name = self.create_index_name(table_name, column_names)
         sql = self.drop_index_string % {
         # 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'
+                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"))
             except KeyError:
                 signals[app_label] = list(model_names)
         # Send only one signal per app.
-        for (app_label, model_names) in signals.iteritems():
+        for (app_label, model_names) in signals.items():
             self.really_send_create_signal(app_label, list(set(model_names)),
                                            verbosity=verbosity,
                                            interactive=interactive)
         """
         
         if self.debug:
-            print " - Sending post_syncdb signal for %s: %s" % (app_label, model_names)
+            print(" - Sending post_syncdb signal for %s: %s" % (app_label, model_names))
         
         app = models.get_app(app_label)
         if not app:

File south/db/oracle.py

+from __future__ import print_function
+
 import os.path
 import sys
 import re
         
         if self.dry_run:
             if self.debug:
-                print '   - no dry run output for alter_column() due to dynamic DDL, sorry'
+                print('   - no dry run output for alter_column() due to dynamic DDL, sorry')
             return
 
         qn = self.quote_name(table_name)
         for sql_template, params in sql_templates:
             try:
                 self.execute(sql_template % params)
-            except DatabaseError, exc:
+            except DatabaseError as exc:
                 description = str(exc)
                 # Oracle complains if a column is already NULL/NOT NULL
                 if 'ORA-01442' in description or 'ORA-01451' in description:

File south/db/postgresql_psycopg2.py

+from __future__ import print_function
+
 import uuid
 from django.db.backends.util import truncate_name
 from south.db import generic
             generic.DatabaseOperations.rename_table(self, old_table_name + "_id_seq", table_name + "_id_seq")
         except:
             if self.debug:
-                print "   ~ No such sequence (ignoring error)"
+                print("   ~ No such sequence (ignoring error)")
             self.rollback_transaction()
         else:
             self.commit_transaction()
             generic.DatabaseOperations.rename_table(self, old_table_name + "_pkey", table_name + "_pkey")
         except:
             if self.debug:
-                print "   ~ No such primary key (ignoring error)"
+                print("   ~ No such primary key (ignoring error)")
             self.rollback_transaction()
         else:
             self.commit_transaction()

File south/db/sql_server/pyodbc.py

 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 south.utils.py3 import string_types
+try:
+    from django.utils.encoding import smart_text                    # Django >= 1.5
+except ImportError:
+    from django.utils.encoding import smart_unicode as smart_text   # Django < 1.5
 from django.core.management.color import no_style
 
 class DatabaseOperations(generic.DatabaseOperations):
         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)
+        if isinstance(value, string_types):
+            return smart_text(value)
         elif isinstance(value, (date,time,datetime)):
             return value.isoformat()
         else:

File south/exceptions.py

+from __future__ import print_function
+
 from traceback import format_exception
 
 class SouthError(RuntimeError):
         self.matches = matches
 
     def __str__(self):
-        self.matches_list = "\n    ".join([unicode(m) for m in self.matches])
+        self.matches_list = "\n    ".join([str(m) for m in self.matches])
         return ("Prefix '%(prefix)s' matches more than one migration:\n"
                 "    %(matches_list)s") % self.__dict__
 
         self.ghosts = ghosts
 
     def __str__(self):
-        self.ghosts_list = "\n    ".join([unicode(m) for m in self.ghosts])
+        self.ghosts_list = "\n    ".join([str(m) for m in self.ghosts])
         return ("\n\n ! These migrations are in the database but not on disk:\n"
                 "    %(ghosts_list)s\n"
                 " ! I'm not trusting myself; either fix this yourself by fiddling\n"
         self.trace = trace
 
     def __str__(self):
-        trace = " -> ".join([unicode(s) for s in self.trace])
+        trace = " -> ".join([str(s) for s in self.trace])
         return ("Found circular dependency:\n"
                 "    %s") % trace
 
         self.depends_on = depends_on
 
     def __str__(self):
-        print "Migration '%(migration)s' depends on unknown migration '%(depends_on)s'." % self.__dict__
+        print("Migration '%(migration)s' depends on unknown migration '%(depends_on)s'." % self.__dict__)
 
 
 class DependsOnUnmigratedApplication(SouthError):

File south/hacks/django_1_0.py

 from django.core.management.commands.flush import Command as FlushCommand
 from django.utils.datastructures import SortedDict
 
+from south.utils.py3 import string_types
+
 class SkipFlushCommand(FlushCommand):
     def handle_noargs(self, **options):
         # no-op to avoid calling flush
         
         # Make sure it contains strings
         if apps:
-            assert isinstance(apps[0], basestring), "The argument to set_installed_apps must be a list of strings."
+            assert isinstance(apps[0], string_types), "The argument to set_installed_apps must be a list of strings."
         
         # Monkeypatch in!
         settings.INSTALLED_APPS, settings.OLD_INSTALLED_APPS = (

File south/logger.py

                 _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."
+            raise IOError("SOUTH_LOGGING_ON is True. You also need a SOUTH_LOGGING_FILE setting.")
     
     return _logger
 

File south/management/commands/convert_to_south.py

 Quick conversion command module.
 """
 
+from __future__ import print_function
+
 from optparse import make_option
 import sys
 
         
         # Make sure we have an app
         if not app:
-            print "Please specify an app to convert."
+            print("Please specify an app to convert.")
             return
         
         # See if the app exists
         try:
             app_module = models.get_app(app)
         except ImproperlyConfigured:
-            print "There is no enabled application matching '%s'." % app
+            print("There is no enabled application matching '%s'." % app)
             return
         
         # Try to get its list of models
         model_list = models.get_models(app_module)
         if not model_list:
-            print "This application has no models; this command is for applications that already have models syncdb'd."
-            print "Make some models, and then use ./manage.py schemamigration %s --initial instead." % app
+            print("This application has no models; this command is for applications that already have models syncdb'd.")
+            print("Make some models, and then use ./manage.py schemamigration %s --initial instead." % app)
             return
         
         # Ask South if it thinks it's already got migrations
         except NoMigrations:
             pass
         else:
-            print "This application is already managed by South."
+            print("This application is already managed by South.")
             return
         
         # Finally! It seems we've got a candidate, so do the two-command trick
             delete_ghosts=options.get("delete_ghosts", False),
         )
         
-        print 
-        print "App '%s' converted. Note that South assumed the application's models matched the database" % app
-        print "(i.e. you haven't changed it since last syncdb); if you have, you should delete the %s/migrations" % app
-        print "directory, revert models.py so it matches the database, and try again."
+        print() 
+        print("App '%s' converted. Note that South assumed the application's models matched the database" % app)
+        print("(i.e. you haven't changed it since last syncdb); if you have, you should delete the %s/migrations" % app)
+        print("directory, revert models.py so it matches the database, and try again.")

File south/management/commands/datamigration.py

 Data migration creation command
 """
 
+from __future__ import print_function
+
 import sys
 import os
 import re
         
         # - is a special name which means 'print to stdout'
         if name == "-":
-            print file_contents
+            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
+            print("Created %s." % new_filename, file=sys.stderr)
     
     def calc_frozen_apps(self, migrations, freeze_list):
         """
         """
         Prints the error, and exits with the given code.
         """
-        print >>sys.stderr, message
+        print(message, file=sys.stderr)
         sys.exit(code)
 
 

File south/management/commands/graphmigrations.py

 Outputs a graphviz dot file of the dependencies.
 """
 
+from __future__ import print_function
+
 from optparse import make_option
 import re
 import textwrap
         color_index = 0
         wrapper = textwrap.TextWrapper(width=40)
         
-        print "digraph G {"
+        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]
+            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 "  }"
+                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.
                     # 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;' % (
+                    print('  "%s.%s" -> "%s.%s" %s;' % (
                         other.app_label(), other.name(),
                         migration.app_label(), migration.name(),
                         attrs
-                    )
+                    ))
             
-        print "}";
+        print("}");

File south/management/commands/migrate.py

 Migrate management command.
 """
 
+from __future__ import print_function
+
 import os.path, re, sys
+from functools import reduce
 from optparse import make_option
 
 from django.core.management.base import BaseCommand
         for app_name in settings.INSTALLED_APPS:
             try:
                 import_module('.management', app_name)
-            except ImportError, exc:
+            except ImportError as exc:
                 msg = exc.args[0]
                 if not msg.startswith('No module named') or 'management' not in msg:
                     raise
             try:
                 apps = [Migrations(app)]
             except NoMigrations:
-                print "The app '%s' does not appear to use migrations." % app
-                print "./manage.py migrate " + self.args
+                print("The app '%s' does not appear to use migrations." % app)
+                print("./manage.py migrate " + self.args)
                 return
         else:
             apps = list(migration.all_migrations())
         applied_migrations = applied_migrations.using(database)
     applied_migration_names = ['%s.%s' % (mi.app_name,mi.migration) for mi in applied_migrations]
 
-    print
+    print()
     for app in apps:
-        print " " + app.app_label()
+        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)
+                print(format_migration_list_item(migration.name(), applied=applied_migration.applied, **options))
             else:
-                print format_migration_list_item(migration.name(), applied=False, **options)
-        print
+                print(format_migration_list_item(migration.name(), applied=False, **options))
+        print()
 
 def show_migration_changes(apps):
     """
         grep "ing " migrations/*.py
     """
     for app in apps:
-        print app.app_label()
+        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
     def field_name(models, model, field):
         return '%s.%s' % (model_name(models, model), field)
         
-    print "  " + migration2.name()
+    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)
+            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)
+            print('    removed model %s' % model_name(models1, model))
             
     # compare models
     for model in models1:
             # find added fields
             for field in models2[model]:
                 if not field in models1[model]:
-                    print '    added field %s' % field_name(models2, model, field)
+                    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)
+                    print('    removed field %s' % field_name(models1, model, field))
                 
             # compare fields
             for field in models1[model]:
                     
                     # 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)
+                        print('    type of %s changed from %s to %s' % (
+                            name, field_value1, field_value2))
                     
                     # if class
                     elif isinstance(field_value1, dict):
                         type2, attr_list2, field_attrs2 = models2[model][field]
                         
                         if type1 != type2:
-                            print '    %s type changed from %s to %s' % (
-                                name, 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)
+                            print('    %s list %s is not []' % (
+                                name, attr_list1))
                         if attr_list2 != []:
-                            print '    %s list %s is not []' % (
-                                name, 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)                
+                            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])
+                                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])
+                                print('    removed attribute %s(%s=%s)' % (
+                                    name, attr, field_attrs1[attr]))
                             
                         # compare field attributes
                         for attr in field_attrs1:
                                 value1 = field_attrs1[attr]
                                 value2 = field_attrs2[attr]
                                 if value1 != value2:
-                                    print '    %s attribute %s changed from %s to %s' % (
-                                        name, attr, value1, value2)
+                                    print('    %s attribute %s changed from %s to %s' % (
+                                        name, attr, value1, value2))
     
     return migration2

File south/management/commands/migrationcheck.py

                     dummy = model._default_manager.exists()
             except (KeyboardInterrupt, SystemExit):
                 raise
-            except Exception, e:
+            except Exception as e:
                 failures += 1
                 if verbosity >= 1:
                     self.stderr.write(err_msg % app_name)

File south/management/commands/schemamigration.py

 Startmigration command, version 2.
 """
 
+from __future__ import print_function
+
 import sys
 import os
 import re
             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."
+                print("You have not passed any of --initial, --auto, --empty, --add-model, --add-field or --add-index.", file=sys.stderr)
                 sys.exit(1)
 
         # Validate this so we can access the last migration without worrying
                     action = action_class(**params)
                     action.add_forwards(forwards_actions)
                     action.add_backwards(backwards_actions)
-                    print >>sys.stderr, action.console_line()
+                    print(action.console_line(), file=sys.stderr)
         
         # Nowt happen? That's not good for --auto.
         if auto and not forwards_actions:
         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()
+                print("Migration to be updated, %s, is already applied, rolling it back now..." % last_migration.name(), file=sys.stderr)
                 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)
 
         # - is a special name which means 'print to stdout'
         if name == "-":
-            print file_contents
+            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.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)
+                print("%s %s. You must now edit this migration and add the code for each direction." % (verb, new_filename), file=sys.stderr)
             else:
-                print >>sys.stderr, "%s %s. You can now apply this migration with: ./manage.py migrate %s" % (verb, new_filename, app)
+                print("%s %s. You can now apply this migration with: ./manage.py migrate %s" % (verb, new_filename, app), file=sys.stderr)
 
 
 MIGRATION_TEMPLATE = """# -*- coding: utf-8 -*-

File south/management/commands/startmigration.py

 Now-obsolete startmigration command.
 """
 
+from __future__ import print_function
+
 from optparse import make_option
 
 from django.core.management.base import BaseCommand
     
     def handle(self, app=None, name="", added_model_list=None, added_field_list=None, initial=False, freeze_list=None, auto=False, stdout=False, added_index_list=None, **options):
         
-        print "The 'startmigration' command is now deprecated; please use the new 'schemamigration' and 'datamigration' commands."
+        print("The 'startmigration' command is now deprecated; please use the new 'schemamigration' and 'datamigration' commands.")

File south/management/commands/syncdb.py

 Overridden syncdb command
 """
 
+from __future__ import print_function
+
 import sys
 from optparse import make_option
 
             for app_name in settings.INSTALLED_APPS:
                 try:
                     import_module('.management', app_name)
-                except ImportError, exc:
+                except ImportError as exc:
                     msg = exc.args[0]
                     if not msg.startswith('No module named') or 'management' not in msg:
                         raise
         
         # Run syncdb on only the ones needed
         if verbosity:
-            print "Syncing..."
+            print("Syncing...")
         
         old_installed, settings.INSTALLED_APPS = settings.INSTALLED_APPS, apps_needing_sync
         old_app_store, cache.app_store = cache.app_store, SortedDict([
         # Migrate if needed
         if options.get('migrate', True):
             if verbosity:
-                print "Migrating..."
+                print("Migrating...")
             management.call_command('migrate', **options)
         
         # Be obvious about what we did
         if verbosity:
-            print "\nSynced:\n > %s" % "\n > ".join(apps_needing_sync)
+            print("\nSynced:\n > %s" % "\n > ".join(apps_needing_sync))
         
         if options.get('migrate', True):
             if verbosity:
-                print "\nMigrated:\n - %s" % "\n - ".join(apps_migrated)
+                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)"
+                print("\nNot synced (use migrations):\n - %s" % "\n - ".join(apps_migrated))
+                print("(use ./manage.py migrate to migrate these)")

File south/migration/__init__.py

 Main migration logic.
 """
 
+from __future__ import print_function
+
 import sys
 
 from django.core.exceptions import ImproperlyConfigured
             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))
+                    print((" ! Migration %s should not have been applied "
+                           "before %s but was." % (last, checking)))
                 result.append((last, checking))
             else:
                 to_check.extend(checking.dependencies)
     
     # 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
+        print("? You have no migrations for the '%s' app. You might want some." % app_label)
         return
     
     # Load the entire dependency graph
         if applied.count() > 1:
             previous_migration = applied[applied.count() - 2]
             if verbosity:
-                print 'previous_migration: %s (applied: %s)' % (previous_migration.migration, previous_migration.applied)
+                print('previous_migration: %s (applied: %s)' % (previous_migration.migration, previous_migration.applied))
             target_name = previous_migration.migration
         else:
             if verbosity:
-                print 'previous_migration: zero'
+                print('previous_migration: zero')
             target_name = 'zero'
     elif target_name == 'current+1':
         try:
     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
+            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,
     else:
         if verbosity:
             # Say there's nothing.
-            print '- Nothing to migrate.'
+            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.

File south/migration/base.py

+from __future__ import print_function
+
 from collections import deque
 import datetime
+from imp import reload
 import os
 import re
 import sys
 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
-
+from south.utils.py3 import string_types, with_metaclass
 
 def all_migrations(applications=None):
     """
 
 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):
+    if isinstance(application, string_types):
         app_label = application.split('.')[-1]
     else:
         app_label = application.__name__.split('.')[-1]
         self.instances = {}
 
 
-class Migrations(list):
+class Migrations(with_metaclass(MigrationsMetaclass, 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
         # 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
+                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
+                print("Creating __init__.py in '%s'..." % migrations_dir)
             open(init_path, "w").close()
     
     def migrations_dir(self):
         return self._cache[name]
 
     def __getitem__(self, value):
-        if isinstance(value, basestring):
+        if isinstance(value, string_types):
             return self.migration(value)
         return super(Migrations, self).__getitem__(value)
 
         return self.app_label() + ':' + self.name()
 
     def __repr__(self):
-        return u'<Migration: %s>' % unicode(self)
+        return '<Migration: %s>' % str(self)
 
     def __eq__(self, other):
         return self.app_label() == other.app_label() and self.name() == other.name()
         except KeyError:
             try:
                 migration = __import__(full_name, {}, {}, ['Migration'])
-            except ImportError, e:
+            except ImportError as e:
                 raise exceptions.UnknownMigration(self, sys.exc_info())
-            except Exception, e:
+            except Exception as e:
                 raise exceptions.BrokenMigration(self, sys.exc_info())
         # Override some imports
         migration._ = lambda x: x  # Fake i18n

File south/migration/migrators.py

+from __future__ import print_function
+
 from copy import copy, deepcopy
-from cStringIO import StringIO
 import datetime
 import inspect
+import io
 import sys
 import traceback
 
 
     def print_title(self, target):
         if self.verbosity:
-            print self.title(target)
+            print(self.title(target))
         
     @staticmethod
     def status(target):
     def print_status(self, migration):
         status = self.status(migration)
         if self.verbosity and status:
-            print status
+            print(status)
 
     @staticmethod
     def orm(migration):
         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
+                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
+                print("Error during commit in migration: %s" % migration)
                 raise
                 
 
     def __init__(self, migrator, *args, **kwargs):
         self._migrator = copy(migrator)
         attributes = dict([(k, getattr(self, k))
-                           for k in self.__class__.__dict__.iterkeys()
+                           for k in self.__class__.__dict__
                            if not k.startswith('__')])
         self._migrator.__dict__.update(attributes)
         self._migrator.__dict__['_wrapper'] = self
     def _run_migration(self, migration):
         if migration.no_dry_run():
             if self.verbosity:
-                print " - Migration '%s' is marked for no-dry-run." % migration
+                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
 class FakeMigrator(MigratorWrapper):
     def run(self, migration):
         if self.verbosity:
-            print '   (faked)'
+            print('   (faked)')
 
     def send_ran_migration(self, *args, **kwargs):
         pass
             return
         # Load initial data, if we ended up at target
         if self.verbosity:
-            print " - Loading initial data for %s." % target.app_label()
+            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
         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()
+        sys.stdout = io.StringIO()
         try:
             try:
                 self.backwards(migration)()

File south/migration/utils.py

     stack = deque(stack)
     while stack:
         try:
-            x = stack[0].next()
-        except AttributeError:
+            x = next(stack[0])
+        except TypeError:
             stack[0] = iter(stack[0])
-            x = stack[0].next()
+            x = next(stack[0])
         except StopIteration:
             stack.popleft()
             continue
-        if hasattr(x, '__iter__'):
+        if hasattr(x, '__iter__') and not isinstance(x, str):
             stack.appendleft(x)
         else:
             yield x

File south/models.py

     def get_migration(self):
         return self.get_migrations().migration(self.migration)
     
-    def __unicode__(self):
+    def __str__(self):
         return "<%s: %s>" % (self.app_name, self.migration)

File south/modelsinspector.py

 rather than direct inspection of models.py.
 """
 
+from __future__ import print_function
+
 import datetime
 import re
 import decimal
 
 from south.utils import get_attribute, auto_through
+from south.utils.py3 import text_type
 
 from django.db import models
 from django.db.models.base import ModelBase, Model
             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
+                closure_contents = value.__closure__[0].cell_contents
                 if closure_contents is None:
                     return "%s.SET_NULL" % (django_db_models_module)
                 # simple function we can perhaps cope with:
     "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."
             
     # Lazy-eval functions get eval'd.
     if isinstance(value, Promise):
-        value = unicode(value)
+        value = text_type(value)
     # If the value is the same as the default, omit it for clarity
     if "default" in options and value == options['default']:
         raise IsDefault
     "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)
+        value = text_type(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
         # Does it define a south_field_triple method?
         if hasattr(field, "south_field_triple"):
             if NOISY:
-                print " ( Nativing field: %s" % field.name
+                print(" ( Nativing field: %s" % field.name)
             field_defs[field.name] = field.south_field_triple()
         # Can we introspect it?
         elif can_introspect(field):
         # Shucks, no definition!
         else:
             if NOISY:
-                print " ( Nodefing field: %s" % field.name
+                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

File south/orm.py

 Roughly emulates the real Django ORM, to a point.
 """
 
+from __future__ import print_function
+
 import inspect
 
 from django.db import models
 from south.utils import ask_for_it_by_name, datetime_utils
 from south.hacks import hacks
 from south.exceptions import UnfreezeMeLater, ORMBaseNotIncluded, ImpossibleORMUnfreeze
+from south.utils.py3 import string_types
 
 
 class ModelsLocals(object):
                 if name == "SouthFieldClass":
                     raise ValueError("Cannot import the required field '%s'" % value)
                 else:
-                    print "WARNING: Cannot import '%s'" % value
+                    print("WARNING: Cannot import '%s'" % value)
         
         # Use ModelsLocals to make lookups work right for CapitalisedModels
         fake_locals = ModelsLocals(fake_locals)
             # OK, add it.
             try:
                 results[key] = self.eval_in_context(code, app)
-            except (NameError, AttributeError), e:
+            except (NameError, AttributeError) as e:
                 raise ValueError("Cannot successfully create meta field '%s' for model '%s.%s': %s." % (
                     key, app, model, e
                 ))
                 key = key.lower()
                 if key not in self.models:
                     raise ORMBaseNotIncluded("Cannot find ORM base %s" % key)
-                elif isinstance(self.models[key], basestring):
+                elif isinstance(self.models[key], string_types):
                     # Then the other model hasn't been unfrozen yet.
                     # We postpone ourselves; the situation will eventually resolve.
                     raise UnfreezeMeLater()
                 continue
             elif not params:
                 raise ValueError("Field '%s' on model '%s.%s' has no definition." % (fname, app, name))
-            elif isinstance(params, (str, unicode)):
+            elif isinstance(params, string_types):
                 # It's a premade definition string! Let's hope it works...
                 code = params
                 extra_imports = {}
                 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:
+                    except (NameError, AttributeError, AssertionError, KeyError) as e:
                         # It's failed again. Complain.
                         raise ValueError("Cannot successfully create field '%s' for model '%s': %s." % (
                             fname, modelname, e

File south/tests/__init__.py

+from __future__ import print_function
 
 #import unittest
 import os
                     testfunc(self)
                 else:
                     # The skip exceptions are not available either...
-                    print "Skipping", testfunc.__name__,"--", message
+                    print("Skipping", testfunc.__name__,"--", message)
             return wrapper
         return decorator
 
             @wraps(testfunc)
             def wrapper(self):
                 if condition:
-                    print "Skipping", testfunc.__name__,"--", message
+                    print("Skipping", testfunc.__name__,"--", message)
                 else:
                     # Apply method
                     testfunc(self)

File south/tests/db.py

 from django.db import connection, models, IntegrityError
 
 from south.tests import unittest, skipIf, skipUnless
+from south.utils.py3 import text_type, with_metaclass
 
 # Create a list of error classes from the various database libraries
 errors = []
         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`))
+        self.assertEqual(value, after, "Change from text to char altered value [ %r != %r ]" % (value, after))
 
     def test_char_to_text(self):
         """
         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`))
+        after = text_type(after) # Oracle text fields return a sort of lazy string -- force evaluation
+        self.assertEqual(value, after, "Change from char to text altered value [ %r != %r ]" % (value, after))
 
     def test_datetime_default(self):
         """
         Datetimes are handled in test_datetime_default.
         """
 
-        class CustomField(models.CharField):
-            __metaclass__ = models.SubfieldBase
+        class CustomField(with_metaclass(models.SubfieldBase, models.CharField)):
             description = 'CustomField'
             def get_default(self):
                 if self.has_default():
             def to_python(self, value):
                 if not value or isinstance(value, list):
                     return value
-                return map(int, value.split(','))
+                return list(map(int, value.split(',')))
 
         false_value = db.has_booleans and 'False' or '0'
         defaults = (

File south/tests/db_mysql.py

 from south.db import db, generic, mysql
 from django.db import connection, models
 
+from south.utils.py3 import with_metaclass
 
-class TestMySQLOperations(unittest.TestCase):
-    """MySQL-specific tests"""
 
-    # A class decoration may be used in lieu of this when Python 2.5 is the
-    # minimum.
-    def __metaclass__(name, bases, dict_):
+# A class decoration may be used in lieu of this when Python 2.5 is the
+# minimum.
+class TestMySQLOperationsMeta(type):
+
+    def __new__(mcs, name, bases, dict_):
         decorator = skipUnless(db.backend_name == "mysql", 'MySQL-specific tests')
 
-        for key, method in dict_.iteritems():
+        for key, method in dict_.items():
             if key.startswith('test'):
                 dict_[key] = decorator(method)
 
-        return type(name, bases, dict_)
+        return type.__new__(mcs, name, bases, dict_)
+
+class TestMySQLOperations(with_metaclass(TestMySQLOperationsMeta, unittest.TestCase)):
+    """MySQL-specific tests"""
 
     def setUp(self):
         db.debug = False

File south/tests/fakeapp/models.py

 class HorribleModel(models.Model):
     "A model to test the edge cases of model parsing"
     
-    ZERO, ONE = range(2)
+    ZERO, ONE = 0, 1
     
     # First, some nice fields
     name = models.CharField(max_length=255)

File south/tests/logger.py

+import io
 import logging
 import os
 import tempfile
 from south.tests import unittest
-import StringIO
 import sys
 
 from django.conf import settings
         settings.SOUTH_LOGGING_ON = False
 
         # Set root logger to capture WARNING and worse
-        logging_stream = StringIO.StringIO()
+        logging_stream = io.StringIO()
         logging.basicConfig(stream=logging_stream, level=logging.WARNING)
 
         db.create_table("test12", [('email_confirmed', models.BooleanField(default=False))])

File south/tests/logic.py

     def assertListEqual(self, list1, list2, msg=None):
         list1 = list(list1)
         list2 = list(list2)
-        list1.sort()
-        list2.sort()
+        try:
+            list1.sort()
+            list2.sort()
+        except TypeError:
+            # emulate Python 2 behavior in Python 3
+            list1 = sorted(list1, key=id)
+            list2 = sorted(list2, key=id)
         return self.assert_(list1 == list2, "%s is not equal to %s" % (list1, list2))
 
     def test_find_ghost_migrations(self):
         
         # We should finish with all migrations
         self.assertListEqual(
-            ((u"fakeapp", u"0001_spam"),
-             (u"fakeapp", u"0002_eggs"),
-             (u"fakeapp", u"0003_alter_spam"),),
+            (("fakeapp", "0001_spam"),
+             ("fakeapp", "0002_eggs"),
+             ("fakeapp", "0003_alter_spam"),),
             MigrationHistory.objects.values_list("app_name", "migration"),
         )
         
         
         # Did it go in?
         self.assertListEqual(
-            ((u"fakeapp", u"0002_eggs"),),
+            (("fakeapp", "0002_eggs"),),
             MigrationHistory.objects.values_list("app_name", "migration"),
         )
         
                           migrations, target_name='zero', fake=False)
         try:
             migrate_app(migrations, target_name=None, fake=False)
-        except exceptions.InconsistentMigrationHistory, e:
+        except exceptions.InconsistentMigrationHistory as e:
             self.assertEqual(
                 [
                     (
             )
         try:
             migrate_app(migrations, target_name="zero", fake=False)
-        except exceptions.InconsistentMigrationHistory, e:
+        except exceptions.InconsistentMigrationHistory as e:
             self.assertEqual(
                 [
                     (
         
         # Nothing should have changed (no merge mode!)
         self.assertListEqual(
-            ((u"fakeapp", u"0002_eggs"),),
+            (("fakeapp", "0002_eggs"),),
             MigrationHistory.objects.values_list("app_name", "migration"),
         )
         
         
         # We should finish with all migrations
         self.assertListEqual(
-            ((u"fakeapp", u"0001_spam"),
-             (u"fakeapp", u"0002_eggs"),
-             (u"fakeapp", u"0003_alter_spam"),),
+            (("fakeapp", "0001_spam"),
+             ("fakeapp", "0002_eggs"),
+             ("fakeapp", "0003_alter_spam"),),
             MigrationHistory.objects.values_list("app_name", "migration"),
         )
         
         migrate_app(migrations, target_name="0002", fake=False)
         self.failIf(null_ok())
         self.assertListEqual(
-            ((u"fakeapp", u"0001_spam"),
-             (u"fakeapp", u"0002_eggs"),),
+            (("fakeapp", "0001_spam"),
+             ("fakeapp", "0002_eggs"),),
             MigrationHistory.objects.values_list("app_name", "migration"),
         )
         
         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"),),
+            (("fakeapp", "0001_spam"),
+             ("fakeapp", "0002_eggs"),
+             ("fakeapp", "0003_alter_spam"),),
             MigrationHistory.objects.values_list("app_name", "migration"),
         )
 
         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"),),
+            (("fakeapp", "0001_spam"),
+             ("fakeapp", "0002_eggs"),),
             MigrationHistory.objects.values_list("app_name", "migration"),
         )
         
         )
         try:
             depends(target, lambda n: graph[n])
-        except exceptions.CircularDependency, e:
+        except exceptions.CircularDependency as e:
             self.assertEqual(trace, e.trace)
 
     def test_depends_cycle(self):

File south/utils/py3.py

+"""
+Python 2 + 3 compatibility functions. This is a very small subset of six.
+"""
+
+import sys
+
+PY3 = sys.version_info[0] == 3
+
+if PY3:
+    string_types = str,
+    text_type = str
+    raw_input = input
+
+else:
+    string_types = basestring,
+    text_type = unicode
+    raw_input = raw_input
+
+def with_metaclass(meta, base=object):
+    """Create a base class with a metaclass."""
+    return meta("NewBase", (base,), {})