Commits

ref...@gmail.com  committed 37722fb Merge

merging with 300

  • Participants
  • Parent commits 2c1421c, e5fbcb6

Comments (0)

Files changed (24)

-regexp:.pyc
+# Globbing is cool.
+syntax: glob
+# No python bytecode
+*.pyc
+2d3621db9559d0a7225448e3e1a25db5180bae15 0.4
+401d26c06d55e61987a99896dd295d589f4d987e 0.5
+1b7449c6d92c4a58c79afc24e4915ee55f205b0c 0.2
+7b837b0502aa246c17bccba54e2e843463e24a22 0.3
+0aa5b5fb6e742e78afc0e59147ad0a662f4bd9e1 stableish
         "Operating System :: OS Independent",
         "Topic :: Software Development"
     ],
-    packages=["south", "south.db", "south.management", "south.management.commands", "south.tests", "south.tests.fakeapp", "south.tests.fakeapp.migrations"],
+    packages=["south", "south.db", "south.hacks", "south.management", "south.management.commands", "south.tests", "south.tests.fakeapp", "south.tests.fakeapp.migrations", "south.tests.otherfakeapp", "south.tests.otherfakeapp.migrations"],
 )

File south/db/generic.py

         Creates the SQL snippet for a column. Used by add_column and add_table.
         """
         qn = connection.ops.quote_name
-
+        
         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)
+        
         sql = field.db_type()
         if sql:        
             field_output = [qn(field.column), sql]
             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 connection.features.supports_tablespaces 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(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
         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 foreign_key_sql(self, from_table_name, from_column_name, to_table_name, to_column_name):
         """

File south/db/mysql.py

         for constraint, itscols in mapping.items():
             if itscols == columns:
                 yield constraint
+    
+    
+    def _field_sanity(self, field):
+        """
+        This particular override stops us sending DEFAULTs for BLOB/TEXT columns.
+        """
+        if field.db_type().upper() in ["BLOB", "TEXT", "LONGTEXT"]:
+            field.null = True
+        return field

File south/hacks/__init__.py

+"""
+The hacks module encapsulates all the horrible things that play with Django
+internals in one, evil place.
+This top file will automagically expose the correct Hacks class.
+"""
+
+# Currently, these work for 1.0 and 1.1.
+from south.hacks.django_1_0 import Hacks
+
+hacks = Hacks()

File south/hacks/django_1_0.py

+"""
+Hacks for the Django 1.0/1.0.2 releases.
+"""
+
+from django.conf import settings
+from django.db import models
+from django.db.models.loading import AppCache, cache
+
+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)
+        
+        # This function will be monkeypatched into place.
+        def new_get_apps():
+            return apps
+        
+        # Monkeypatch in!
+        models.get_apps_old, models.get_apps = models.get_apps, new_get_apps
+        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.
+        """
+        models.get_apps = models.get_apps_old
+        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.
+        """
+        a = AppCache()
+        a.loaded = False
+        a._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
+    

File south/management/commands/migrate.py

+"""
+Migrate management command.
+"""
+
+import sys
+from optparse import make_option
+
 from django.core.management.base import BaseCommand
 from django.core.management.color import no_style
 from django.conf import settings
 from django.db import models
-from optparse import make_option
+
 from south import migration
-import sys
 
 class Command(BaseCommand):
     option_list = BaseCommand.option_list + (
             help='Verbosity level; 0=minimal output, 1=normal output, 2=all output'),
         )
     help = "Runs migrations for all apps."
-    usage_str = "Usage: ./manage.py migrate [appname] [migrationname|zero] [--all] [--list] [--skip] [--merge] [--no-initial-data] [--fake] [--db-dry-run]"
+    args = "[appname] [migrationname|zero] [--all] [--list] [--skip] [--merge] [--no-initial-data] [--fake] [--db-dry-run]"
 
     def handle(self, app=None, target=None, skip=False, merge=False, backwards=False, fake=False, db_dry_run=False, list=False, **options):
 

File south/management/commands/startmigration.py

         forwards = ""
         backwards = ""
         frozen_models = {} # Frozen models, used by the Fake ORM
-        stub_models = {} # Frozen models, but only enough for relation ends (old mock models)
         complete_apps = set() # Apps that are completely frozen - useable for diffing.
         
         # Sets of actions
                     # Get everything in an app!
                     frozen_models.update(dict([(x, None) for x in models.get_models(models.get_app(item))]))
                     complete_apps.add(item.split(".")[-1])
-            # For every model in the freeze list, add in dependency stubs
-            for model in frozen_models:
-                stub_models.update(model_dependencies(model))
+            # For every model in the freeze list, add in frozen dependencies
+            for model in list(frozen_models):
+                frozen_models.update(model_dependencies(model))
         
         
         ### Automatic Detection ###
             
             model = model_unkey(mkey)
             
-            # Add the model's dependencies to the stubs
-            stub_models.update(model_dependencies(model))
+            # Add the model's dependencies to the frozens
+            frozen_models.update(model_dependencies(model))
             # Get the field definitions
             fields = modelsinspector.get_model_fields(model)
             # Turn the (class, args, kwargs) format into a string
             # ManyToMany fields need special attention.
             if isinstance(field, models.ManyToManyField):
                 if not field.rel.through: # Bug #120
-                    # Add a stub model for each side
-                    stub_models[model] = None
-                    stub_models[field.rel.to] = None
+                    # Add a frozen model for each side
+                    frozen_models[model] = None
+                    frozen_models[field.rel.to] = None
                     # And a field defn, that's actually a table creation
                     forwards += CREATE_M2MFIELD_SNIPPET % (
                         model._meta.object_name,
             print " + Added field '%s.%s'" % (mkey, field_name)
             
             # Add any dependencies
-            stub_models.update(field_dependencies(field))
+            frozen_models.update(field_dependencies(field))
             
             # Work out the definition
             triple = remove_useless_attributes(
             
             # ManyToMany fields need special attention.
             if isinstance(field, models.ManyToManyField):
-                # Add a stub model for each side, if they're not already there
+                # Add a frozen model for each side, if they're not already there
                 # (if we just added old versions, we might override new ones)
-                if model not in stub_models:
-                    stub_models[model] = last_models
+                if model not in frozen_models:
+                    frozen_models[model] = last_models
                 if field.rel.to not in last_models:
-                    stub_models[field.rel.to] = last_models
+                    frozen_models[field.rel.to] = last_models
                 # And a field defn, that's actually a table deletion
                 forwards += DELETE_M2MFIELD_SNIPPET % (
                     model._meta.object_name,
             
             # Add any dependencies
             deps = field_dependencies(field, last_models)
-            deps.update(stub_models)
-            stub_models = deps
+            deps.update(frozen_models)
+            frozen_models = deps
             
             # Work out the definition
             triple = remove_useless_attributes(triple)
             
             print " - Deleted model '%s.%s'" % (model._meta.app_label,model._meta.object_name)
             
-            # Add the model's dependencies to the stubs
+            # Add the model's dependencies to the frozens
             deps = model_dependencies(model, last_models)
-            deps.update(stub_models)
-            stub_models = deps
+            deps.update(frozen_models)
+            frozen_models = deps
             
             # Turn the (class, args, kwargs) format into a string
             fields = triples_to_defs(app, model, fields)
         for model, last_models in frozen_models.items():
             all_models[model_key(model)] = prep_for_freeze(model, last_models)
         
-        # Fill out stub model definitions
-        for model, last_models in stub_models.items():
-            key = model_key(model)
-            if key in all_models:
-                continue # We'd rather use full models than stubs.
-            all_models[key] = prep_for_stub(model, last_models)
-        
         # Do some model cleanup, and warnings
         for modelname, model in all_models.items():
             for fieldname, fielddef in model.items():
         fields = last_models[model_key(model)]
     else:
         fields = modelsinspector.get_model_fields(model, m2m=True)
+    # Remove _stub if it stuck in
+    if "_stub" in fields:
+        del fields["_stub"]
     # Remove useless attributes (like 'choices')
     for name, field in fields.items():
+        if name == "Meta":
+            continue
         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
     return fields
 
 
-def prep_for_stub(model, last_models=None):
-    if last_models:
-        fields = last_models[model_key(model)]
-    else:
-        fields = modelsinspector.get_model_fields(model)
-    # Now, take only the PK (and a 'we're a stub' field) and freeze 'em
-    pk = model._meta.pk.name
-    fields = {
-        pk: ormise_triple(model._meta.pk, remove_useless_attributes(fields[pk])),
-        "_stub": True,
-    }
-    # Meta is important too.
-    if last_models:
-        meta = last_models[model_key(model)].get("Meta", {})
-    else:
-        meta = modelsinspector.get_model_meta(model)
-    if meta:
-        fields['Meta'] = remove_useless_meta(meta)
-    return fields
-
-
 ### Module handling functions
 
 def model_key(model):
 
 ### Dependency resolvers
 
-def model_dependencies(model, last_models=None):
+def model_dependencies(model, last_models=None, checked_models=None):
     """
     Returns a set of models this one depends on to be defined; things like
     OneToOneFields as ID, ForeignKeys everywhere, etc.
     """
     depends = {}
+    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, last_models))
+    # Now recurse
+    new_to_check = set(depends.keys()) - 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, last_models, checked_models)
+        # Loop through dependencies...
+        for dep, value in deps.items():
+            # 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[dep] = value
     return depends
 
-def stub_model_dependencies(model, last_models=None):
-    """
-    Returns a set of models this one depends on to be defined as a stub model
-    (i.e. deps of the PK).
-    """
-    return field_dependencies(model._meta.pk, last_models)
 
 def field_dependencies(field, last_models=None):
     depends = {}
-    if isinstance(field, (models.OneToOneField, models.ForeignKey, models.ManyToManyField)):
+    if isinstance(field, (models.OneToOneField, models.ForeignKey, models.ManyToManyField, GenericRelation)):
         depends[field.rel.to] = last_models
-        depends.update(stub_model_dependencies(field.rel.to, last_models))
+        depends.update(field_dependencies(field.rel.to._meta.pk, last_models))
     return depends
     
 
     if old[0] != new[0]:
         if old[0].startswith("models.") and (new[0].startswith("django.db.models") \
          or new[0].startswith("django.contrib.gis")):
-            return old[0].split(".")[-1] != new[0].split(".")[-1] 
+            if old[0].split(".")[-1] != new[0].split(".")[-1]:
+                return True
     # If the third bits or end of second are different, it really is different.
     if old[2] != new[2] or old[1][1:] != new[1][1:]:
         return True

File south/migration.py

+"""
+Main migration logic.
+"""
 
 import datetime
 import os
 import sys
 import traceback
 import inspect
+
 from django.conf import settings
 from django.db import models
 from django.core.exceptions import ImproperlyConfigured
 from django.core.management import call_command
-from models import MigrationHistory
+
+from south.models import MigrationHistory
 from south.db import db
 from south.orm import FakeORM
-
+from south.signals import *
 
 def get_app(app):
     """
     # Annotate tree with 'backwards edges'
     for app, classes in tree.items():
         for name, cls in classes.items():
-            cls.needs = []
-            if not hasattr(cls, "needed_by"):
-                cls.needed_by = []
+            if not hasattr(cls, "_dependency_parents"):
+                cls._dependency_parents = []
+            if not hasattr(cls, "_dependency_children"):
+                cls._dependency_children = []
+            # Get forwards dependencies
             if hasattr(cls, "depends_on"):
                 for dapp, dname in cls.depends_on:
                     dapp = get_app(dapp)
                             get_app_name(dapp),
                         )
                         sys.exit(1)
-                    cls.needs.append((dapp, dname))
-                    if not hasattr(tree[dapp][dname], "needed_by"):
-                        tree[dapp][dname].needed_by = []
-                    tree[dapp][dname].needed_by.append((app, name))
+                    cls._dependency_parents.append((dapp, dname))
+                    if not hasattr(tree[dapp][dname], "_dependency_children"):
+                        tree[dapp][dname]._dependency_children = []
+                    tree[dapp][dname]._dependency_children.append((app, name))
+            # Get backwards dependencies
+            if hasattr(cls, "needed_by"):
+                for dapp, dname in cls.needed_by:
+                    dapp = get_app(dapp)
+                    if dapp not in tree:
+                        print "Migration %s in app %s claims to be needed by unmigrated app %s." % (
+                            name,
+                            get_app_name(app),
+                            dapp,
+                        )
+                        sys.exit(1)
+                    if dname not in tree[dapp]:
+                        print "Migration %s in app %s claims to be needed by nonexistent migration %s in app %s." % (
+                            name,
+                            get_app_name(app),
+                            dname,
+                            get_app_name(dapp),
+                        )
+                        sys.exit(1)
+                    cls._dependency_children.append((dapp, dname))
+                    if not hasattr(tree[dapp][dname], "_dependency_parents"):
+                        tree[dapp][dname]._dependency_parents = []
+                    tree[dapp][dname]._dependency_parents.append((app, name))
     
     # Sanity check whole tree
     for app, classes in tree.items():
     # Get the dependencies of a migration
     deps = []
     migration = tree[app][name]
-    for dapp, dname in migration.needs:
+    for dapp, dname in migration._dependency_parents:
         deps.extend(
             dependencies(tree, dapp, dname, trace+[(app,name)])
         )
         for aname in app_migrations[:app_migrations.index(name)]:
             needed += needed_before_forwards(tree, app, aname, False)
             needed += [(app, aname)]
-    for dapp, dname in tree[app][name].needs:
+    for dapp, dname in tree[app][name]._dependency_parents:
         needed += needed_before_forwards(tree, dapp, dname)
         needed += [(dapp, dname)]
     return remove_duplicates(needed)
         for aname in reversed(app_migrations[app_migrations.index(name)+1:]):
             needed += needed_before_backwards(tree, app, aname, False)
             needed += [(app, aname)]
-    for dapp, dname in tree[app][name].needed_by:
+    for dapp, dname in tree[app][name]._dependency_children:
         needed += needed_before_backwards(tree, dapp, dname)
         needed += [(dapp, dname)]
     return remove_duplicates(needed)
         if not db_dry_run:
             # Record us as having done this
             recorder(app_name, migration)
+            if not fake:
+                # Send a signal saying it ran
+                ran_migration.send(None, app=app_name, migration=migration, method=torun)
 
 
 def run_forwards(app, migrations, fake=False, db_dry_run=False, silent=False):
 def migrate_app(app, target_name=None, resolve_mode=None, fake=False, db_dry_run=False, yes=False, silent=False, load_inital_data=False, skip=False):
     
     app_name = get_app_name(app)
+    db.debug = not silent
     
-    db.debug = not silent
+    # Fire off the pre-migrate signal
+    pre_migrate.send(None, app=app_name)
     
     # Find out what delightful migrations we have
     tree = dependency_tree()
     else:
         if not silent:
             print "- Nothing to migrate."
+    
+    # Finally, fire off the post-migrate signal
+    post_migrate.send(None, app=app_name)

File south/modelsinspector.py

 """
 
 import modelsparser
+from south.utils import get_attribute
 
 from django.db import models
+from django.contrib.localflavor import us
 from django.db.models.base import ModelBase
 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
 
 NOISY = True
 
         [],
         {
             "null": ["null", {"default": False}],
-            "blank": ["blank", {"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}],
         },
     ),
     (
+        (models.ManyToManyField,),
+        [],
+        {
+            "to": ["rel.to", {}],
+        },
+    ),
+    (
         (models.DateField, models.TimeField),
         [],
         {
             "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}],
+        },
+    ),
 ]
 
 # Similar, but for Meta, so just the inner level (kwds).
         return True
     # Check it's a core field (one I've written for)
     module = field.__class__.__module__
-    return module.startswith("django.db")
+    return module.startswith("django.db") or module.startswith("django.contrib.gis") or module.startswith("django.contrib.localflavor") or module.startswith("django.contrib.contenttypes.generic")
 
 
 def matching_details(field):
     """
 
 
-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 get_value(field, descriptor):
     """
     Gets an attribute value from a Field instance and formats it.
     """
     attrname, options = descriptor
     value = get_attribute(field, attrname)
+    # Lazy-eval functions get eval'd.
+    # Annoyingly, we can't do an isinstance() test
+    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
     # 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
     # Models get their own special repr()
-    if type(value) is ModelBase:
+    if isinstance(value, ModelBase):
         return "orm['%s.%s']" % (value._meta.app_label, value._meta.object_name)
     # Callables get called.
     elif callable(value):

File south/orm.py

 from django.db.models.loading import cache
 
 from south.db import db
+from south.utils import ask_for_it_by_name
+from south.hacks import hacks
 
 
 class ModelsLocals(object):
         except AttributeError:
             return
         
+        # Start a 'new' AppCache
+        hacks.clear_app_cache()
+        
         # Now, make each model's data into a FakeModel
         for name, data in self.models_source.items():
             # Make sure there's some kind of Meta
         
         # 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 __getattr__(self, key):
                 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)
+            return model._meta.get_field_by_name(fname)[0]
         else:
             return model
     
     
-    def eval_in_context(self, code, app):
+    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)
         # Datetime; there should be no datetime direct accesses
         fake_locals['datetime'] = datetime
         
+        # 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:
+                print "WARNING: Cannot import '%s'" % value
+        
         # Use ModelsLocals to make lookups work right for CapitalisedModels
         fake_locals = ModelsLocals(fake_locals)
         
         
         # 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 isinstance(params, (str, unicode)):
                 # It's a premade definition string! Let's hope it works...
                 code = params
-            elif len(params) == 1:
-                code = "%s()" % params[0]
-            elif len(params) == 3:
-                code = "%s(%s)" % (
-                    params[0],
-                    ", ".join(
+                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()]
-                    ),
-                )
-            else:
-                raise ValueError("Field '%s' on model '%s.%s' has a weird definition length (should be 1 or 3 items)." % (fname, app, name))
+                    )
+                    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:
-                field = self.eval_in_context(code, app)
+                # 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
+                failed_fields[fname] = (code, extra_imports)
             else:
                 fields[fname] = field
         
         
         more_kwds['Meta'] = meta
         
-        # Stop AppCache from changing!
-        cache.app_models[app], old_app_models = {}, cache.app_models.get(app, {})
-        
         # Make our model
         fields.update(more_kwds)
         
             fields,
         )
         
-        # Send AppCache back in time
-        cache.app_models[app] = old_app_models
-        
         # If this is a stub model, change Objects to a whiny class
         if stub:
             model.objects = WhinyManager()
         for modelkey, model in self.models.items():
             app, modelname = modelkey.split(".", 1)
             if hasattr(model, "_failed_fields"):
-                for fname, code in model._failed_fields.items():
+                for fname, (code, extra_imports) in model._failed_fields.items():
                     try:
-                        field = self.eval_in_context(code, app)
+                        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." % (
         return getattr(self.real, name)
 
 
-def ask_for_it_by_name(name):
-    "Returns an object referenced by absolute path."
-    bits = name.split(".")
-    modulename = ".".join(bits[:-1])
-    module = __import__(modulename, {}, {}, bits[-1])
-    return getattr(module, bits[-1])
-
-
 def whiny_method(*a, **kw):
     raise ValueError("You cannot instantiate a stub model.")

File south/signals.py

+"""
+South-specific signals
+"""
+
+from django.dispatch import Signal
+
+# 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"])

File south/tests/__init__.py

 
 import unittest
-from django.conf import settings
+
+from south.hacks import hacks
 
 # Note: the individual test files are imported below this.
 
         return fake
     
     
-    def monkeypatch(self):
-        """Swaps out various Django calls for fake ones for our own nefarious purposes."""
-        
-        def new_get_apps():
-            return ['fakeapp']
-        
-        from django.db import models
-        from django.conf import settings
-        models.get_apps_old, models.get_apps = models.get_apps, new_get_apps
-        settings.INSTALLED_APPS, settings.OLD_INSTALLED_APPS = (
-            ["fakeapp"],
-            settings.INSTALLED_APPS,
-        )
-        self.redo_app_cache()
-    setUp = monkeypatch
+    def setUp(self):
+        """
+        Changes the Django environment so we can run tests against our test apps.
+        """
+        hacks.set_installed_apps(["fakeapp", "otherfakeapp"])
     
     
-    def unmonkeypatch(self):
-        """Undoes what monkeypatch did."""
-        
-        from django.db import models
-        from django.conf import settings
-        models.get_apps = models.get_apps_old
-        settings.INSTALLED_APPS = settings.OLD_INSTALLED_APPS
-        self.redo_app_cache()
-    tearDown = unmonkeypatch
-    
-    
-    def redo_app_cache(self):
-        from django.db.models.loading import AppCache
-        a = AppCache()
-        a.loaded = False
-        a._populate()
+    def tearDown(self):
+        """
+        Undoes what setUp did.
+        """
+        hacks.reset_installed_apps()
+
 
 # Try importing all tests if asked for (then we can run 'em)
 try:
 
 if not skiptest:
     from south.tests.db import *
-    from south.tests.logic import *
-    from south.tests.modelsparser import *
+    from south.tests.logic import *

File south/tests/fakeapp/migrations/0003_alter_spam.py

 
 class Migration:
     
+    needed_by = (
+        ("otherfakeapp", "0003_third"),
+    )
+    
     def forwards(self):
         
         db.alter_column("southtest_spam", 'name', models.CharField(max_length=255, null=True))

File south/tests/logic.py

 
 from south import migration
 from south.tests import Monkeypatcher
+from south.utils import snd
 
 # Add the tests directory so fakeapp is on sys.path
 test_root = os.path.dirname(__file__)
     def test_get_migrated_apps(self):
         
         P1 = __import__("fakeapp.migrations", {}, {}, [''])
+        P2 = __import__("otherfakeapp.migrations", {}, {}, [''])
         
         self.assertEqual(
-            [P1],
+            [P1,P2],
             list(migration.get_migrated_apps()),
         )
     
     def test_all_migrations(self):
         
         app = migration.get_app("fakeapp")
+        otherapp = migration.get_app("otherfakeapp")
         
-        self.assertEqual(
-            {app: {
-                "0001_spam": migration.get_migration(app, "0001_spam"),
-                "0002_eggs": migration.get_migration(app, "0002_eggs"),
-                "0003_alter_spam": migration.get_migration(app, "0003_alter_spam"),
-            }},
+        self.assertEqual({
+                app: {
+                    "0001_spam": migration.get_migration(app, "0001_spam"),
+                    "0002_eggs": migration.get_migration(app, "0002_eggs"),
+                    "0003_alter_spam": migration.get_migration(app, "0003_alter_spam"),
+                },
+                otherapp: {
+                    "0001_first": migration.get_migration(otherapp, "0001_first"),
+                    "0002_second": migration.get_migration(otherapp, "0002_second"),
+                    "0003_third": migration.get_migration(otherapp, "0003_third"),
+                },
+            },
             migration.all_migrations(),
         )
     
         
         # finish with no migrations, otherwise other tests fail...
         migration.migrate_app(app, target_name="zero", resolve_mode=None, fake=False, silent=True)
-        self.assertEqual(list(migration.MigrationHistory.objects.all()), [])
+        self.assertEqual(list(migration.MigrationHistory.objects.all()), [])
+    
+    def test_dependencies(self):
+        
+        fakeapp = migration.get_app("fakeapp")
+        otherfakeapp = migration.get_app("otherfakeapp")
+        
+        # Test a simple path
+        tree = migration.dependency_tree()
+        self.assertEqual(
+            map(snd, migration.needed_before_forwards(tree, fakeapp, "0003_alter_spam")),
+            ['0001_spam', '0002_eggs'],
+        )
+        
+        # And a complex one, with both back and forwards deps
+        self.assertEqual(
+            map(snd, migration.needed_before_forwards(tree, otherfakeapp, "0003_third")),
+            ['0001_spam', '0001_first', '0002_second', '0002_eggs', '0003_alter_spam'],
+        )

File south/tests/modelsparser.py

-# -*- coding: UTF-8 -*-
-
-import unittest
-
-from south.db import db
-from south.tests import Monkeypatcher
-from south.tests.fakeapp.models import HorribleModel, Other1, Other2
-
-from south.modelsparser import get_model_fields, get_model_meta
-
-class TestModelParsing(Monkeypatcher):
-
-    """
-    Tests parsing of models.py files against the test one.
-    """
-    
-    def test_fields(self):
-        
-        fields = get_model_fields(HorribleModel)
-        self.assertEqual(
-            fields,
-            {
-                'id': ('models.AutoField', [], {'primary_key': 'True'}),
-                
-                'name': ('models.CharField', [], {'max_length': '255'}), 
-                'short_name': ('models.CharField', [], {'max_length': '50'}),
-                'slug': ('models.SlugField', [], {'unique': 'True'}), 
-                
-                'o1': ('models.ForeignKey', ['Other1'], {}),
-                'o2': ('models.ForeignKey', ["'Other2'"], {}),
-                
-                'user': ('models.ForeignKey', ['User'], {'related_name': '"horribles"'}),
-                
-                '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': "default_func", 'max_length': '25'}),
-                
-                'choiced': ('models.CharField', [], {'max_length': '20', 'choices': 'choices'}),
-                
-                'multiline': ('models.TextField', [], {}),
-            },
-        )
-        
-        fields2 = get_model_fields(Other2)
-        self.assertEqual(
-            fields2,
-            {'close_but_no_cigar': ('models.PositiveIntegerField', [], {'primary_key': 'True'})},
-        )
-        
-        fields3 = get_model_fields(Other1)
-        self.assertEqual(
-            fields3,
-            {'id': ('models.AutoField', [], {'primary_key': 'True'})},
-        )
-    
-    
-    def test_meta(self):
-        
-        meta = get_model_meta(HorribleModel)
-        self.assertEqual(
-            meta,
-            {'db_table': '"my_fave"', 'verbose_name': '"Dr. Strangelove,"+"""or how I learned to stop worrying\nand love the bomb"""'},
-        )

File south/tests/otherfakeapp/__init__.py

Empty file added.

File south/tests/otherfakeapp/migrations/0001_first.py

+from south.db import db
+from django.db import models
+
+class Migration:
+    
+    depends_on = (
+        ("fakeapp", "0001_spam"),
+    )
+    
+    def forwards(self):
+        pass
+    
+    def backwards(self):
+        pass
+

File south/tests/otherfakeapp/migrations/0002_second.py

+from south.db import db
+from django.db import models
+
+class Migration:
+    
+    def forwards(self):
+        pass
+    
+    def backwards(self):
+        pass
+

File south/tests/otherfakeapp/migrations/0003_third.py

+from south.db import db
+from django.db import models
+
+class Migration:
+    
+    def forwards(self):
+        pass
+    
+    def backwards(self):
+        pass

File south/tests/otherfakeapp/migrations/__init__.py

Empty file added.

File south/tests/otherfakeapp/models.py

+# This file left intentionally blank.

File south/utils.py

+"""
+Generally helpful utility functions.
+"""
+
+
+def ask_for_it_by_name(name):
+    "Returns an object referenced by absolute path."
+    bits = name.split(".")
+    modulename = ".".join(bits[:-1])
+    module = __import__(modulename, {}, {}, bits[-1])
+    return getattr(module, bits[-1])
+
+
+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
+
+
+fst = lambda (x, y): x
+snd = lambda (x, y): y