Commits

Andrew Godwin  committed a79bc8c Merge

Branch merge

  • Participants
  • Parent commits 271d6a4, ad394df
  • Branches noparsing

Comments (0)

Files changed (12)

 
 setup(
     name='South',
-    version='0.5',
+    version='0.5.1',
     description='South: Migrations for Django',
     long_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.',
     author='Andrew Godwin & Andy McCurdy',

File south/__init__.py

 South - Useable migrations for Django apps
 """
 
-__version__ = "0.5"
+__version__ = "0.5.1"
 __authors__ = ["Andrew Godwin <andrew@aeracode.org>", "Andy McCurdy <andy@andymccurdy.com>"]

File south/db/__init__.py

 # This code somewhat lifted from django evolution
 from django.conf import settings
 import sys
-module_name = ['south.db', settings.DATABASE_ENGINE]
+if hasattr(settings, "SOUTH_DATABASE_ADAPTER"):
+    module_name = settings.SOUTH_DATABASE_ADAPTER
+else:
+    module_name = '.'.join(['south.db', settings.DATABASE_ENGINE])
+
 try:
-    module = __import__('.'.join(module_name),{},{},[''])
+    module = __import__(module_name,{},{},[''])
 except ImportError:
-    sys.stderr.write("There is no South database module for the engine '%s'. Please either choose a supported one, or remove South from INSTALLED_APPS.\n" % settings.DATABASE_ENGINE)
+    sys.stderr.write("There is no South database module for the engine '%s' (tried with %s). Please either choose a supported one, or check for SOUTH_DATABASE_ADAPTER settings, or remove South from INSTALLED_APPS.\n" 
+                     % (settings.DATABASE_ENGINE, module_name))
     sys.exit(1)
-db = module.DatabaseOperations()
+db = module.DatabaseOperations()

File south/db/sqlite3.py

         """
         raise NotImplementedError("SQLite does not support renaming columns.")
     
+    # Nor unique creation
+    def create_unique(self, table_name, columns):
+        """
+        Not supported under SQLite.
+        """
+        print "WARNING: SQLite does not support adding unique constraints. Ignored."
+    
+    # Nor unique deletion
+    def delete_unique(self, table_name, columns):
+        """
+        Not supported under SQLite.
+        """
+        print "WARNING: SQLite does not support removing unique constraints. Ignored."
+    
     # No cascades on deletes
     def delete_table(self, table_name, cascade=True):
         generic.DatabaseOperations.delete_table(self, table_name, False)

File south/management/commands/startmigration.py

 except NameError:
     from sets import Set as set
 
-from south import migration, modelsparser, modelsinspector
+from south import migration, modelsparser
 
 
 class Command(BaseCommand):
         ### Added fields ###
         for mkey, field_name in added_fields:
             
-            print " + Added field '%s.%s'" % (mkey, field_name)
-            
             # Get the model
             model = model_unkey(mkey)
             # Get the field
                         field.name,
                         field.m2m_db_table(),
                         field.m2m_column_name()[:-3], # strip off the '_id' at the end
-                        model._meta.object_name,
+                        poss_ormise(app, model, model._meta.object_name),
                         field.m2m_reverse_name()[:-3], # strip off the '_id' at the ned
-                        field.rel.to._meta.object_name
+                        poss_ormise(app, field.rel.to, field.rel.to._meta.object_name)
                         )
                     backwards += DELETE_M2MFIELD_SNIPPET % (
                         model._meta.object_name,
                         field.name,
                         field.m2m_db_table()
                     )
+                    print " + Added M2M '%s.%s'" % (mkey, field_name)
                 continue
             
             # GenericRelations need ignoring
             if isinstance(field, GenericRelation):
                 continue
             
+            print " + Added field '%s.%s'" % (mkey, field_name)
+            
             # Add any dependencies
             stub_models.update(field_dependencies(field))
             
                     field.name,
                     field.m2m_db_table(),
                     field.m2m_column_name()[:-3], # strip off the '_id' at the end
-                    model._meta.object_name,
+                    poss_ormise(app, model, model._meta.object_name),
                     field.m2m_reverse_name()[:-3], # strip off the '_id' at the ned
-                    field.rel.to._meta.object_name
+                    poss_ormise(app, field.rel.to, field.rel.to._meta.object_name)
                     )
                 continue
             
 
 ### Cleaning functions for freezing
 
+
+def ormise_triple(field, triple):
+    "Given a 'triple' definition, runs poss_ormise on each arg."
+    
+    # If it's a string defn, return it plain.
+    if not isinstance(triple, (list, tuple)):
+        return triple
+    
+    # For each arg, if it's a related type, try ORMising it.
+    args = []
+    for arg in triple[1]:
+        if hasattr(field, "rel") and hasattr(field.rel, "to") and field.rel.to:
+            args.append(poss_ormise(None, field.rel.to, arg))
+        else:
+            args.append(arg)
+    
+    return (triple[0], args, triple[2])
+
+
 def prep_for_freeze(model, last_models=None):
     if last_models:
         fields = last_models[model_key(model)]
     else:
         fields = modelsparser.get_model_fields(model, m2m=True)
-        print modelsinspector.get_model_fields(model, m2m=True)
     # Remove useless attributes (like 'choices')
     for name, field in fields.items():
-        fields[name] = remove_useless_attributes(field)
+        real_field = model._meta.get_field_by_name(name)[0]
+        fields[name] = ormise_triple(real_field, remove_useless_attributes(field))
     # See if there's a Meta
     if last_models:
         meta = last_models[model_key(model)].get("Meta", {})
     # Now, take only the PK (and a 'we're a stub' field) and freeze 'em
     pk = model._meta.pk.name
     fields = {
-        pk: remove_useless_attributes(fields[pk]),
+        pk: ormise_triple(model._meta.pk, remove_useless_attributes(fields[pk])),
         "_stub": True,
     }
     # Meta is important too.
 
 def model_key(model):
     "For a given model, return 'appname.modelname'."
-    return ("%s.%s" % (model._meta.app_label, model._meta.object_name)).lower()
+    return "%s.%s" % (model._meta.app_label, model._meta.object_name.lower())
 
 def model_unkey(key):
     "For 'appname.modelname', return the model."
 ### Output sanitisers
 
 
-USELESS_KEYWORDS = ["choices", "help_text", "upload_to"]
+USELESS_KEYWORDS = ["choices", "help_text", "upload_to", "verbose_name"]
 USELESS_DB_KEYWORDS = ["related_name"] # Important for ORM, not for DB.
 
 def remove_useless_attributes(field, db=False):
 def make_field_constructor(default_app, field, triple):
     """
     Given the defualt app, the field class,
-    and the defn triple (or string), make the defition string.
+    and the defn triple (or string), make the definition string.
     """
     # It might be a defn string already...
     if isinstance(triple, (str, unicode)):
                     added_fields.add((key, fieldname))
             # For the ones that exist in both models, see if they were changed
             for fieldname in still_there:
-                if fieldname != "Meta" and \
-                   remove_useless_attributes(new[key][fieldname], True) != \
-                   remove_useless_attributes(old[key][fieldname], True):
+                if fieldname != "Meta" and different_attributes(
+                   remove_useless_attributes(old[key][fieldname], True),
+                   remove_useless_attributes(new[key][fieldname], True)):
                     changed_fields.append((key, fieldname, old[key][fieldname], new[key][fieldname]))
     
     return added_models, deleted_models, continued_models, added_fields, deleted_fields, changed_fields
 
 
+# Backwards-compat comparison that ignores orm. on the RHS and not the left
+def different_attributes(old, new):
+    # If they're not triples, just do normal comparison
+    if not isinstance(old, (list, tuple)) or not isinstance(new, (list, tuple)):
+        return old != new
+    # If the first or third bits or end of second are different, it really is different.
+    if old[0] != new[0] or old[2] != new[2] or old[1][1:] != new[1][1:]:
+        return True
+    if not old[1] and not new[1]:
+        return False
+    # Compare first positional arg
+    if "orm" in new[1][0] and "orm" not in old[1][0]:
+        # Do special comparison to fix #153
+        try:
+            return old[1][0] != new[1][0].split("'")[1].split(".")[1]
+        except IndexError:
+            pass # Fall back to next comparison
+    return old[1][0] != new[1][0]
+    
+    
+
+
 def meta_diff(old, new):
     """
     Diffs the two provided Meta definitions (dicts).

File south/management/commands/test.py

 from django.core.management.commands import syncdb
 from django.conf import settings
 
+from syncdb import Command as SyncDbCommand
+
+
+class MigrateAndSyncCommand(SyncDbCommand):
+    option_list = SyncDbCommand.option_list
+    for opt in option_list:
+        if "--migrate" == opt.get_opt_string():
+            opt.default = True
+            break
+
+
 class Command(test.Command):
     
     def handle(self, *args, **kwargs):
+        management.get_commands()
         if not hasattr(settings, "SOUTH_TESTS_MIGRATE") or 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.get_commands()
             management._commands['syncdb'] = 'django.core'
+        else:
+            management._commands['syncdb'] = MigrateAndSyncCommand()
         super(Command, self).handle(*args, **kwargs)

File south/management/commands/testserver.py

+from django.core import management
+from django.core.management.commands import testserver
+from django.core.management.commands import syncdb
+from django.conf import settings
+
+from syncdb import Command as SyncDbCommand
+
+
+class MigrateAndSyncCommand(SyncDbCommand):
+    option_list = SyncDbCommand.option_list
+    for opt in option_list:
+        if "--migrate" == opt.get_opt_string():
+            opt.default = True
+            break
+
+
+class Command(testserver.Command):
+    
+    def handle(self, *args, **kwargs):
+        management.get_commands()
+        if not hasattr(settings, "SOUTH_TESTS_MIGRATE") or 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()
+        super(Command, self).handle(*args, **kwargs)

File south/migration.py

         migclass = module.Migration
         migclass.orm = FakeORM(migclass, get_app_name(app))
         module._ = lambda x: x  # Fake i18n
+        module.datetime = datetime
         return migclass
     except ImportError:
         print " ! Migration %s:%s probably doesn't exist." % (get_app_name(app), name)

File south/modelsparser.py

 import symbol
 import token
 import keyword
+import datetime
 
 from django.db import models
+from django.contrib.contenttypes import generic
+from django.utils.datastructures import SortedDict
+from django.core.exceptions import ImproperlyConfigured
 
 
 def name_that_thing(thing):
         return " " + repr(tree)
 
 
+def isclass(obj):
+    "Simple test to see if something is a class."
+    return issubclass(type(obj), type)
+
+
+def aliased_models(module):
+    """
+    Given a models module, returns a dict mapping all alias imports of models
+    (e.g. import Foo as Bar) back to their original names. Bug #134.
+    """
+    aliases = {}
+    for name, obj in module.__dict__.items():
+        if isclass(obj) and issubclass(obj, models.Model) and obj is not models.Model:
+            # Test to see if this has a different name to what it should
+            if name != obj._meta.object_name:
+                aliases[name] = obj._meta.object_name
+    return aliases
+    
+
+
 class STTree(object):
     
     "A syntax tree wrapper class."
     possible_field_defs = tree.find("^ > classdef > suite > stmt > simple_stmt > small_stmt > expr_stmt")
     field_defs = {}
     
+    # Get aliases, ready for alias fixing (#134)
+    try:
+        aliases = aliased_models(models.get_app(model._meta.app_label))
+    except ImproperlyConfigured:
+        aliases = {}
+    
     # Go through all the found defns, and try to parse them
     for pfd in possible_field_defs:
         field = extract_field(pfd)
     # Go through all bases (that are themselves models, but not Model)
     for base in model.__bases__:
         if base != models.Model and issubclass(base, models.Model):
-            inherited_fields.update(get_model_fields(base))
+            inherited_fields.update(get_model_fields(base, m2m))
     
     # 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
-    fields = {}
+    fields = SortedDict()
     for field in source:
         # Get its name
         fieldname = field.name
-        if isinstance(field, models.related.RelatedObject):
+        if isinstance(field, (models.related.RelatedObject, generic.GenericRel)):
             continue
         # Now, try to get the defn
         if fieldname in field_defs:
         else:
             fields[fieldname] = None
     
-    # Now, try seeing if we can resolve the values of defaults.
+    # Now, try seeing if we can resolve the values of defaults, and fix aliases.
     for field, defn in fields.items():
         
         if not isinstance(defn, (list, tuple)):
             continue # We don't have a defn for this one, or it's a string
         
+        # Fix aliases if we can (#134)
+        for i, arg in enumerate(defn[1]):
+            if arg in aliases:
+                defn[1][i] = aliases[arg]
+        
+        # Fix defaults if we can
         for arg, val in defn[2].items():
             if arg in ['default']:
                 try:
                 # Hm, OK, we got a value. Callables are not frozen (see #132, #135)
                 else:
                     if callable(real_val):
-                        pass
+                        # HACK
+                        # However, if it's datetime.now, etc., that's special
+                        for datetime_key in datetime.datetime.__dict__.keys():
+                            # No, you can't use __dict__.values. It's different.
+                            dtm = getattr(datetime.datetime, datetime_key)
+                            if real_val == dtm:
+                                if not val.startswith("datetime.datetime"):
+                                    defn[2][arg] = "datetime." + val
+                                break
                     else:
                         defn[2][arg] = repr(real_val)
         

File south/orm.py

         # And a fake _ function
         fake_locals['_'] = lambda x: x
         
+        # Datetime; there should be no datetime direct accesses
+        fake_locals['datetime'] = datetime
+        
         # Use ModelsLocals to make lookups work right for CapitalisedModels
         fake_locals = ModelsLocals(fake_locals)
         
         more_kwds['Meta'] = meta
         
         # Stop AppCache from changing!
-        cache.app_models[app], old_app_models = {}, cache.app_models[app]
+        cache.app_models[app], old_app_models = {}, cache.app_models.get(app, {})
         
         # Make our model
         fields.update(more_kwds)

File south/tests/fakeapp/models.py

 # -*- coding: UTF-8 -*-
 
 from django.db import models
-from django.contrib.auth.models import User
+from django.contrib.auth.models import User as UserAlias
 
 def default_func():
     return "yays"
     o2 = models.ForeignKey('Other2')
     
     # Now to something outside
-    user = models.ForeignKey(User, related_name="horribles")
+    user = models.ForeignKey(UserAlias, related_name="horribles")
     
     # Unicode!
     code = models.CharField(max_length=25, default="↑↑↓↓←→←→BA")

File south/tests/modelsparser.py

                 'code': ('models.CharField', [], {'max_length': '25', 'default': "'\\xe2\\x86\\x91\\xe2\\x86\\x91\\xe2\\x86\\x93\\xe2\\x86\\x93\\xe2\\x86\\x90\\xe2\\x86\\x92\\xe2\\x86\\x90\\xe2\\x86\\x92BA'"}),
                 
                 'class_attr': ('models.IntegerField', [], {'default': '0'}),
-                'func': ('models.CharField', [], {'default': "'yays'", 'max_length': '25'}),
+                'func': ('models.CharField', [], {'default': "default_func", 'max_length': '25'}),
                 
                 'choiced': ('models.CharField', [], {'max_length': '20', 'choices': 'choices'}),