Author: russellm
Date: 2007-09-21 11:19:20 -0500 (Fri, 21 Sep 2007)
New Revision: 6400

Added:
   django/trunk/tests/modeltests/user_commands/
   django/trunk/tests/modeltests/user_commands/__init__.py
   django/trunk/tests/modeltests/user_commands/management/
   django/trunk/tests/modeltests/user_commands/management/__init__.py
   django/trunk/tests/modeltests/user_commands/management/commands/
   django/trunk/tests/modeltests/user_commands/management/commands/__init__.py
   django/trunk/tests/modeltests/user_commands/management/commands/dance.py
   django/trunk/tests/modeltests/user_commands/models.py
Modified:
   django/trunk/django/core/management/__init__.py
   django/trunk/django/core/management/base.py
   django/trunk/docs/django-admin.txt
Log:
Fixed #5516 -- Added the ability for applications to define their own 
management commands. Pieces of this patch taken from a contribution by Todd 
O'Bryan. Thanks Todd.


Modified: django/trunk/django/core/management/__init__.py
===================================================================
--- django/trunk/django/core/management/__init__.py     2007-09-21 04:00:32 UTC 
(rev 6399)
+++ django/trunk/django/core/management/__init__.py     2007-09-21 16:19:20 UTC 
(rev 6400)
@@ -1,19 +1,101 @@
 import django
+from django.core.management.base import BaseCommand, CommandError, 
handle_default_options 
 from optparse import OptionParser
 import os
 import sys
+from imp import find_module
 
 # For backwards compatibility: get_version() used to be in this module.
 get_version = django.get_version
 
-def load_command_class(name):
+# A cache of loaded commands, so that call_command 
+# doesn't have to reload every time it is called
+_commands = None
+
+def find_commands(management_dir):
     """
-    Given a command name, returns the Command class instance. Raises
-    ImportError if it doesn't exist.
+    Given a path to a management directory, return a list of all the command 
names 
+    that are available. Returns an empty list if no commands are defined.
     """
-    # Let the ImportError propogate.
-    return getattr(__import__('django.core.management.commands.%s' % name, {}, 
{}, ['Command']), 'Command')()
+    command_dir = os.path.join(management_dir,'commands')
+    try:
+        return [f[:-3] for f in os.listdir(command_dir) if not 
f.startswith('_') and f.endswith('.py')]
+    except OSError:
+        return []
 
+def find_management_module(app_name):
+    """
+    Determine the path to the management module for the application named,
+    without acutally 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()
+    path = None
+    while parts:
+        part = parts.pop()
+        f,path,descr = find_module(part, path and [path] or None)
+    return path
+    
+def load_command_class(app_name, name):
+    """
+    Given a command name and an application name, returns the Command 
+    class instance. All errors raised by the importation process
+    (ImportError, AttributeError) are allowed to propagate.
+    """
+    return getattr(__import__('%s.management.commands.%s' % (app_name, name), 
+                   {}, {}, ['Command']), 'Command')()
+
+def get_commands(load_user_commands=True, project_directory=None):
+    """
+    Returns a dictionary of commands against the application in which
+    those commands can be found. 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; user-defined commands will also
+    be included if ``load_user_commands`` is True. If a project directory
+    is provided, the startproject command will be disabled, and the
+    startapp command will be modified to use that directory.
+
+    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)
+    
+    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(__path__[0])])
+        if load_user_commands:
+            # Get commands from all installed apps
+            from django.conf import settings
+            for app_name in settings.INSTALLED_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
+
 def call_command(name, *args, **options):
     """
     Calls the given command, with the given options and args/kwargs.
@@ -25,8 +107,22 @@
         call_command('shell', plain=True)
         call_command('sqlall', 'myapp')
     """
-    klass = load_command_class(name)
+    try:
+        app_name = get_commands()[name]
+        klass = load_command_class(app_name, name)
+    except KeyError:
+        raise CommandError, "Unknown command: %r" % name
     return klass.execute(*args, **options)
+    
+class LaxOptionParser(OptionParser): 
+    """
+    An option parser that doesn't raise any errors on unknown options.
+    
+    This is needed because the --settings and --pythonpath options affect
+    the commands (and thus the options) that are available to the user. 
+    """
+    def error(self, msg): 
+           pass    
 
 class ManagementUtility(object):
     """
@@ -38,21 +134,9 @@
     def __init__(self, argv=None):
         self.argv = argv or sys.argv[:]
         self.prog_name = os.path.basename(self.argv[0])
-        self.commands = self.default_commands()
-
-    def default_commands(self):
-        """
-        Returns a dictionary of instances of all available Command classes.
-
-        This works by looking for and loading all Python modules in the
-        django.core.management.commands package.
-
-        The dictionary is in the format {name: command_instance}.
-        """
-        command_dir = os.path.join(__path__[0], 'commands')
-        names = [f[:-3] for f in os.listdir(command_dir) if not 
f.startswith('_') and f.endswith('.py')]
-        return dict([(name, load_command_class(name)) for name in names])
-
+        self.project_directory = None
+        self.user_commands = False
+        
     def main_help_text(self):
         """
         Returns the script's main help text, as a string.
@@ -61,7 +145,7 @@
         usage.append('Django command line tool, version %s' % 
django.get_version())
         usage.append("Type '%s help <subcommand>' for help on a specific 
subcommand." % self.prog_name)
         usage.append('Available subcommands:')
-        commands = self.commands.keys()
+        commands = get_commands(self.user_commands, 
self.project_directory).keys()
         commands.sort()
         for cmd in commands:
             usage.append('  %s' % cmd)
@@ -74,16 +158,26 @@
         django-admin.py or manage.py) if it can't be found.
         """
         try:
-            return self.commands[subcommand]
+            app_name = get_commands(self.user_commands, 
self.project_directory)[subcommand]
+            klass = load_command_class(app_name, subcommand)
         except KeyError:
             sys.stderr.write("Unknown command: %r\nType '%s help' for 
usage.\n" % (subcommand, self.prog_name))
             sys.exit(1)
-
+        return klass
+        
     def execute(self):
         """
         Given the command-line arguments, this figures out which subcommand is
         being run, creates a parser appropriate to that command, and runs it.
         """
+        # Preprocess options to extract --settings and --pythonpath. These 
options
+        # could affect the commands that are available, so they must be 
processed
+        # early
+        parser = LaxOptionParser(version=get_version(), 
+                                 option_list=BaseCommand.option_list) 
+        options, args = parser.parse_args(self.argv) 
+        handle_default_options(options)
+         
         try:
             subcommand = self.argv[1]
         except IndexError:
@@ -91,8 +185,8 @@
             sys.exit(1)
 
         if subcommand == 'help':
-            if len(self.argv) > 2:
-                self.fetch_command(self.argv[2]).print_help(self.prog_name, 
self.argv[2])
+            if len(args) > 2:
+                self.fetch_command(args[2]).print_help(self.prog_name, args[2])
             else:
                 sys.stderr.write(self.main_help_text() + '\n')
                 sys.exit(1)
@@ -116,16 +210,9 @@
     """
     def __init__(self, argv, project_directory):
         super(ProjectManagementUtility, self).__init__(argv)
-
-        # Remove the "startproject" command from self.commands, because
-        # that's a django-admin.py command, not a manage.py command.
-        del self.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
-        self.commands['startapp'] = ProjectCommand(project_directory)
-
+        self.project_directory = project_directory
+        self.user_commands = True
+                
 def setup_environ(settings_mod):
     """
     Configure the runtime environment. This can also be used by external

Modified: django/trunk/django/core/management/base.py
===================================================================
--- django/trunk/django/core/management/base.py 2007-09-21 04:00:32 UTC (rev 
6399)
+++ django/trunk/django/core/management/base.py 2007-09-21 16:19:20 UTC (rev 
6400)
@@ -9,6 +9,17 @@
 class CommandError(Exception):
     pass
 
+def handle_default_options(options):
+    """
+    Include any default options that all commands should accept
+    here so that ManagementUtility can handle them before searching
+    for user commands.
+    """
+    if options.settings:
+        os.environ['DJANGO_SETTINGS_MODULE'] = options.settings
+    if options.pythonpath:
+        sys.path.insert(0, options.pythonpath)
+                
 class BaseCommand(object):
     # Metadata about this command.
     option_list = (
@@ -55,10 +66,7 @@
     def run_from_argv(self, argv):
         parser = self.create_parser(argv[0], argv[1])
         options, args = parser.parse_args(argv[2:])
-        if options.settings:
-            os.environ['DJANGO_SETTINGS_MODULE'] = options.settings
-        if options.pythonpath:
-            sys.path.insert(0, options.pythonpath)
+        handle_default_options(options)
         self.execute(*args, **options.__dict__)
 
     def execute(self, *args, **options):

Modified: django/trunk/docs/django-admin.txt
===================================================================
--- django/trunk/docs/django-admin.txt  2007-09-21 04:00:32 UTC (rev 6399)
+++ django/trunk/docs/django-admin.txt  2007-09-21 16:19:20 UTC (rev 6400)
@@ -735,3 +735,32 @@
     * Press [TAB] to see all available options.
     * Type ``sql``, then [TAB], to see all available options whose names start
       with ``sql``.
+
+Customized actions
+==================
+
+**New in Django development version**
+
+If you want to add an action of your own to ``manage.py``, you can.
+Simply add a ``management/commands`` directory to your application.
+Each python module in that directory will be discovered and registered as
+a command that can be executed as an action when you run ``manage.py``::
+
+    /fancy_blog
+        __init__.py
+        models.py
+        /management
+            __init__.py
+            /commands
+                __init__.py
+                explode.py
+        views.py
+        
+In this example, ``explode`` command will be made available to any project
+that includes the ``fancy_blog`` application in ``settings.INSTALLED_APPS``.
+
+The ``explode.py`` module has only one requirement -- it must define a class
+called ``Command`` that extends ``django.core.management.base.BaseCommand``.
+
+For more details on how to define your own commands, look at the code for the
+existing ``django-admin.py`` commands, in ``/django/core/management/commands``.


Property changes on: django/trunk/tests/modeltests/user_commands
___________________________________________________________________
Name: svn:ignore
   + *.pyc


Added: django/trunk/tests/modeltests/user_commands/__init__.py
===================================================================


Property changes on: django/trunk/tests/modeltests/user_commands/management
___________________________________________________________________
Name: svn:ignore
   + *.pyc


Added: django/trunk/tests/modeltests/user_commands/management/__init__.py
===================================================================


Property changes on: 
django/trunk/tests/modeltests/user_commands/management/commands
___________________________________________________________________
Name: svn:ignore
   + *.pyc


Added: 
django/trunk/tests/modeltests/user_commands/management/commands/__init__.py
===================================================================

Added: django/trunk/tests/modeltests/user_commands/management/commands/dance.py
===================================================================
--- django/trunk/tests/modeltests/user_commands/management/commands/dance.py    
                        (rev 0)
+++ django/trunk/tests/modeltests/user_commands/management/commands/dance.py    
2007-09-21 16:19:20 UTC (rev 6400)
@@ -0,0 +1,9 @@
+from django.core.management.base import BaseCommand
+
+class Command(BaseCommand):
+    help = "Dance around like a madman."
+    args = ''
+    requires_model_validation = True
+
+    def handle(self, *args, **options):
+        print "I don't feel like dancing."
\ No newline at end of file

Added: django/trunk/tests/modeltests/user_commands/models.py
===================================================================
--- django/trunk/tests/modeltests/user_commands/models.py                       
        (rev 0)
+++ django/trunk/tests/modeltests/user_commands/models.py       2007-09-21 
16:19:20 UTC (rev 6400)
@@ -0,0 +1,30 @@
+"""
+37. User-registered management commands
+
+The manage.py utility provides a number of useful commands for managing a
+Django project. If you want to add a utility command of your own, you can.
+
+The user-defined command 'dance' is defined in the management/commands 
+subdirectory of this test application. It is a simple command that responds 
+with a printed message when invoked.
+
+For more details on how to define your own manage.py commands, look at the
+django.core.management.commands directory. This directory contains the
+definitions for the base Django manage.py commands.
+"""
+
+__test__ = {'API_TESTS': """
+>>> from django.core import management
+
+# Invoke a simple user-defined command
+>>> management.call_command('dance')
+I don't feel like dancing.
+
+# Invoke a command that doesn't exist
+>>> management.call_command('explode')
+Traceback (most recent call last):
+...
+CommandError: Unknown command: 'explode'
+
+
+"""}
\ No newline at end of file


--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to [EMAIL PROTECTED]
For more options, visit this group at 
http://groups.google.com/group/django-updates?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to