commit 18a544f51d549809459bef7237df3e8982b33fdd
Author: Kostas Papadimitriou <kpap@grnet.gr>
Date:   Mon Jan 16 13:56:01 2012 +0200

    Management utility fix
    
    Handle command discovery of namespaced application modules.

diff --git a/snf-webproject/synnefo/webproject/manage.py b/snf-webproject/synnefo/webproject/manage.py
index 81cd8e2..d615f3e 100755
--- a/snf-webproject/synnefo/webproject/manage.py
+++ b/snf-webproject/synnefo/webproject/manage.py
@@ -1,13 +1,158 @@
-#!/usr/bin/env python
+"""
+Extented django management module
+
+Most of the code is shared from django.core.management module
+to allow us extend the default django ManagementUtility object
+used to provide command line interface of the django project
+included in snf-webproject package.
+
+The extended class provides the following:
+
+- additional argument for the configuration of the SYNNEFO_SETTINGS_DIR
+  environmental variable (--settings-dir).
+- a fix for management utility to handle custom commands defined in
+  applications living in namespaced packages (django ticket #14087)
+- override of --version command to display the snf-webproject version
+"""
 
 from django.core.management import ManagementUtility, setup_environ, \
-BaseCommand, LaxOptionParser, handle_default_options
+BaseCommand, LaxOptionParser, handle_default_options, find_commands, \
+load_command_class
 
+from django.core import management
+from django.utils.importlib import import_module
 from optparse import Option, make_option
 from synnefo.util.version import get_component_version
 
 import sys
 import os
+import imp
+
+_commands = None
+
+def find_modules(name, path=None):
+    """Find all modules with name 'name'
+
+    Unlike find_module in the imp package this returns a list of all
+    matched modules.
+    """
+    results = []
+    if path is None: path = sys.path
+    for p in path:
+        importer = sys.path_importer_cache.get(p, None)
+        if importer is None:
+            find_module = imp.find_module
+        else:
+            find_module = importer.find_module
+
+        try:
+            result = find_module(name, [p])
+            if result is not None:
+                results.append(result)
+        except ImportError:
+            pass
+    if not results:
+        raise ImportError("No module named %.200s" % name)
+    return results
+
+def find_management_module(app_name):
+    """
+    Determines the path to the management module for the given app_name,
+    without actually importing the application or the management module.
+
+    Raises ImportError if the management module cannot be found for any reason.
+    """
+    parts = app_name.split('.')
+    parts.append('management')
+    parts.reverse()
+    part = parts.pop()
+    paths = None
+
+    # When using manage.py, the project module is added to the path,
+    # loaded, then removed from the path. This means that
+    # testproject.testapp.models can be loaded in future, even if
+    # testproject isn't in the path. When looking for the management
+    # module, we need look for the case where the project name is part
+    # of the app_name but the project directory itself isn't on the path.
+    try:
+        modules = find_modules(part, paths)
+        paths = [m[1] for m in modules]
+    except ImportError,e:
+        if os.path.basename(os.getcwd()) != part:
+            raise e
+
+    while parts:
+        part = parts.pop()
+        modules = find_modules(part, paths)
+        paths = [m[1] for m in modules]
+    return paths[0]
+
+
+def get_commands():
+    """
+    Returns a dictionary mapping command names to their callback applications.
+
+    This works by looking for a management.commands package in django.core, and
+    in each installed application -- if a commands package exists, all commands
+    in that package are registered.
+
+    Core commands are always included. If a settings module has been
+    specified, user-defined commands will also be included, the
+    startproject command will be disabled, and the startapp command
+    will be modified to use the directory in which the settings module appears.
+
+    The dictionary is in the format {command_name: app_name}. Key-value
+    pairs from this dictionary can then be used in calls to
+    load_command_class(app_name, command_name)
+
+    If a specific version of a command must be loaded (e.g., with the
+    startapp command), the instantiated module can be placed in the
+    dictionary in place of the application name.
+
+    The dictionary is cached on the first call and reused on subsequent
+    calls.
+    """
+    global _commands
+    if _commands is None:
+        _commands = dict([(name, 'django.core') for name in \
+            find_commands(management.__path__[0])])
+
+        # Find the installed apps
+        try:
+            from django.conf import settings
+            apps = settings.INSTALLED_APPS
+        except (AttributeError, EnvironmentError, ImportError):
+            apps = []
+
+        # Find the project directory
+        try:
+            from django.conf import settings
+            module = import_module(settings.SETTINGS_MODULE)
+            project_directory = setup_environ(module, settings.SETTINGS_MODULE)
+        except (AttributeError, EnvironmentError, ImportError, KeyError):
+            project_directory = None
+
+        # Find and load the management module for each installed app.
+        for app_name in apps:
+            try:
+                path = find_management_module(app_name)
+                _commands.update(dict([(name, app_name)
+                                       for name in find_commands(path)]))
+            except ImportError:
+                pass # No management module - ignore this app
+
+        if project_directory:
+            # Remove the "startproject" command from self.commands, because
+            # that's a django-admin.py command, not a manage.py command.
+            del _commands['startproject']
+
+            # Override the startapp command so that it always uses the
+            # project_directory, not the current working directory
+            # (which is default).
+            from django.core.management.commands.startapp import ProjectCommand
+            _commands['startapp'] = ProjectCommand(project_directory)
+
+    return _commands
 
 class SynnefoManagementUtility(ManagementUtility):
     """
@@ -76,6 +221,37 @@ class SynnefoManagementUtility(ManagementUtility):
         else:
             self.fetch_command(subcommand).run_from_argv(self.argv)
 
+    def main_help_text(self):
+        """
+        Returns the script's main help text, as a string.
+        """
+        usage = ['',"Type '%s help <subcommand>' for help on a specific subcommand." % self.prog_name,'']
+        usage.append('Available subcommands:')
+        commands = get_commands().keys()
+        commands.sort()
+        for cmd in commands:
+            usage.append('  %s' % cmd)
+        return '\n'.join(usage)
+
+    def fetch_command(self, subcommand):
+        """
+        Tries to fetch the given subcommand, printing a message with the
+        appropriate command called from the command line (usually
+        "django-admin.py" or "manage.py") if it can't be found.
+        """
+        try:
+            app_name = get_commands()[subcommand]
+        except KeyError:
+            sys.stderr.write("Unknown command: %r\nType '%s help' for usage.\n" % \
+                (subcommand, self.prog_name))
+            sys.exit(1)
+        if isinstance(app_name, BaseCommand):
+            # If the command is already loaded, use it directly.
+            klass = app_name
+        else:
+            klass = load_command_class(app_name, subcommand)
+        return klass
+
 def main():
     # no need to run setup_environ
     # we already know our project
