Django

Code

Changeset 6400

Show
Ignore:
Timestamp:
09/21/07 11:19:20 (10 months ago)
Author:
russellm
Message:

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.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • django/trunk/django/core/management/base.py

    r6281 r6400  
    1010    pass 
    1111 
     12def handle_default_options(options): 
     13    """ 
     14    Include any default options that all commands should accept 
     15    here so that ManagementUtility can handle them before searching 
     16    for user commands. 
     17    """ 
     18    if options.settings: 
     19        os.environ['DJANGO_SETTINGS_MODULE'] = options.settings 
     20    if options.pythonpath: 
     21        sys.path.insert(0, options.pythonpath) 
     22                 
    1223class BaseCommand(object): 
    1324    # Metadata about this command. 
     
    5667        parser = self.create_parser(argv[0], argv[1]) 
    5768        options, args = parser.parse_args(argv[2:]) 
    58         if options.settings: 
    59             os.environ['DJANGO_SETTINGS_MODULE'] = options.settings 
    60         if options.pythonpath: 
    61             sys.path.insert(0, options.pythonpath) 
     69        handle_default_options(options) 
    6270        self.execute(*args, **options.__dict__) 
    6371 
  • django/trunk/django/core/management/__init__.py

    r6093 r6400  
    11import django 
     2from django.core.management.base import BaseCommand, CommandError, handle_default_options  
    23from optparse import OptionParser 
    34import os 
    45import sys 
     6from imp import find_module 
    57 
    68# For backwards compatibility: get_version() used to be in this module. 
    79get_version = django.get_version 
    810 
    9 def load_command_class(name): 
    10     """ 
    11     Given a command name, returns the Command class instance. Raises 
    12     ImportError if it doesn't exist. 
    13     """ 
    14     # Let the ImportError propogate. 
    15     return getattr(__import__('django.core.management.commands.%s' % name, {}, {}, ['Command']), 'Command')() 
     11# A cache of loaded commands, so that call_command  
     12# doesn't have to reload every time it is called 
     13_commands = None 
     14 
     15def find_commands(management_dir): 
     16    """ 
     17    Given a path to a management directory, return a list of all the command names  
     18    that are available. Returns an empty list if no commands are defined. 
     19    """ 
     20    command_dir = os.path.join(management_dir,'commands') 
     21    try: 
     22        return [f[:-3] for f in os.listdir(command_dir) if not f.startswith('_') and f.endswith('.py')] 
     23    except OSError: 
     24        return [] 
     25 
     26def find_management_module(app_name): 
     27    """ 
     28    Determine the path to the management module for the application named, 
     29    without acutally importing the application or the management module. 
     30 
     31    Raises ImportError if the management module cannot be found for any reason. 
     32    """ 
     33    parts = app_name.split('.') 
     34    parts.append('management') 
     35    parts.reverse() 
     36    path = None 
     37    while parts: 
     38        part = parts.pop() 
     39        f,path,descr = find_module(part, path and [path] or None) 
     40    return path 
     41     
     42def load_command_class(app_name, name): 
     43    """ 
     44    Given a command name and an application name, returns the Command  
     45    class instance. All errors raised by the importation process 
     46    (ImportError, AttributeError) are allowed to propagate. 
     47    """ 
     48    return getattr(__import__('%s.management.commands.%s' % (app_name, name),  
     49                   {}, {}, ['Command']), 'Command')() 
     50 
     51def get_commands(load_user_commands=True, project_directory=None): 
     52    """ 
     53    Returns a dictionary of commands against the application in which 
     54    those commands can be found. This works by looking for a  
     55    management.commands package in django.core, and in each installed  
     56    application -- if a commands package exists, all commands in that 
     57    package are registered. 
     58 
     59    Core commands are always included; user-defined commands will also 
     60    be included if ``load_user_commands`` is True. If a project directory 
     61    is provided, the startproject command will be disabled, and the 
     62    startapp command will be modified to use that directory. 
     63 
     64    The dictionary is in the format {command_name: app_name}. Key-value 
     65    pairs from this dictionary can then be used in calls to  
     66    load_command_class(app_name, command_name) 
     67     
     68    The dictionary is cached on the first call, and reused on subsequent 
     69    calls. 
     70    """ 
     71    global _commands 
     72    if _commands is None: 
     73        _commands = dict([(name, 'django.core')  
     74                          for name in find_commands(__path__[0])]) 
     75        if load_user_commands: 
     76            # Get commands from all installed apps 
     77            from django.conf import settings 
     78            for app_name in settings.INSTALLED_APPS: 
     79                try: 
     80                    path = find_management_module(app_name) 
     81                    _commands.update(dict([(name, app_name)  
     82                                           for name in find_commands(path)])) 
     83                except ImportError: 
     84                    pass # No management module - ignore this app 
     85                     
     86        if project_directory: 
     87            # Remove the "startproject" command from self.commands, because 
     88            # that's a django-admin.py command, not a manage.py command. 
     89            del _commands['startproject'] 
     90 
     91            # Override the startapp command so that it always uses the 
     92            # project_directory, not the current working directory  
     93            # (which is default). 
     94            from django.core.management.commands.startapp import ProjectCommand 
     95            _commands['startapp'] = ProjectCommand(project_directory) 
     96 
     97    return _commands 
    1698 
    1799def call_command(name, *args, **options): 
     
    26108        call_command('sqlall', 'myapp') 
    27109    """ 
    28     klass = load_command_class(name) 
     110    try: 
     111        app_name = get_commands()[name] 
     112        klass = load_command_class(app_name, name) 
     113    except KeyError: 
     114        raise CommandError, "Unknown command: %r" % name 
    29115    return klass.execute(*args, **options) 
     116     
     117class LaxOptionParser(OptionParser):  
     118    """ 
     119    An option parser that doesn't raise any errors on unknown options. 
     120     
     121    This is needed because the --settings and --pythonpath options affect 
     122    the commands (and thus the options) that are available to the user.  
     123    """ 
     124    def error(self, msg):  
     125            pass     
    30126 
    31127class ManagementUtility(object): 
     
    39135        self.argv = argv or sys.argv[:] 
    40136        self.prog_name = os.path.basename(self.argv[0]) 
    41         self.commands = self.default_commands() 
    42  
    43     def default_commands(self): 
    44         """ 
    45         Returns a dictionary of instances of all available Command classes. 
    46  
    47         This works by looking for and loading all Python modules in the 
    48         django.core.management.commands package. 
    49  
    50         The dictionary is in the format {name: command_instance}. 
    51         """ 
    52         command_dir = os.path.join(__path__[0], 'commands') 
    53         names = [f[:-3] for f in os.listdir(command_dir) if not f.startswith('_') and f.endswith('.py')] 
    54         return dict([(name, load_command_class(name)) for name in names]) 
    55  
     137        self.project_directory = None 
     138        self.user_commands = False 
     139         
    56140    def main_help_text(self): 
    57141        """ 
     
    62146        usage.append("Type '%s help <subcommand>' for help on a specific subcommand." % self.prog_name) 
    63147        usage.append('Available subcommands:') 
    64         commands = self.commands.keys() 
     148        commands = get_commands(self.user_commands, self.project_directory).keys() 
    65149        commands.sort() 
    66150        for cmd in commands: 
     
    75159        """ 
    76160        try: 
    77             return self.commands[subcommand] 
     161            app_name = get_commands(self.user_commands, self.project_directory)[subcommand] 
     162            klass = load_command_class(app_name, subcommand) 
    78163        except KeyError: 
    79164            sys.stderr.write("Unknown command: %r\nType '%s help' for usage.\n" % (subcommand, self.prog_name)) 
    80165            sys.exit(1) 
    81  
     166        return klass 
     167         
    82168    def execute(self): 
    83169        """ 
     
    85171        being run, creates a parser appropriate to that command, and runs it. 
    86172        """ 
     173        # Preprocess options to extract --settings and --pythonpath. These options 
     174        # could affect the commands that are available, so they must be processed 
     175        # early 
     176        parser = LaxOptionParser(version=get_version(),  
     177                                 option_list=BaseCommand.option_list)  
     178        options, args = parser.parse_args(self.argv)  
     179        handle_default_options(options) 
     180          
    87181        try: 
    88182            subcommand = self.argv[1] 
     
    92186 
    93187        if subcommand == 'help': 
    94             if len(self.argv) > 2: 
    95                 self.fetch_command(self.argv[2]).print_help(self.prog_name, self.argv[2]) 
     188            if len(args) > 2: 
     189                self.fetch_command(args[2]).print_help(self.prog_name, args[2]) 
    96190            else: 
    97191                sys.stderr.write(self.main_help_text() + '\n') 
     
    117211    def __init__(self, argv, project_directory): 
    118212        super(ProjectManagementUtility, self).__init__(argv) 
    119  
    120         # Remove the "startproject" command from self.commands, because 
    121         # that's a django-admin.py command, not a manage.py command. 
    122         del self.commands['startproject'] 
    123  
    124         # Override the startapp command so that it always uses the 
    125         # project_directory, not the current working directory (which is default). 
    126         from django.core.management.commands.startapp import ProjectCommand 
    127         self.commands['startapp'] = ProjectCommand(project_directory) 
    128  
     213        self.project_directory = project_directory 
     214        self.user_commands = True 
     215                 
    129216def setup_environ(settings_mod): 
    130217    """ 
  • django/trunk/docs/django-admin.txt

    r6363 r6400  
    736736    * Type ``sql``, then [TAB], to see all available options whose names start 
    737737      with ``sql``. 
     738 
     739Customized actions 
     740================== 
     741 
     742**New in Django development version** 
     743 
     744If you want to add an action of your own to ``manage.py``, you can. 
     745Simply add a ``management/commands`` directory to your application. 
     746Each python module in that directory will be discovered and registered as 
     747a command that can be executed as an action when you run ``manage.py``:: 
     748 
     749    /fancy_blog 
     750        __init__.py 
     751        models.py 
     752        /management 
     753            __init__.py 
     754            /commands 
     755                __init__.py 
     756                explode.py 
     757        views.py 
     758         
     759In this example, ``explode`` command will be made available to any project 
     760that includes the ``fancy_blog`` application in ``settings.INSTALLED_APPS``. 
     761 
     762The ``explode.py`` module has only one requirement -- it must define a class 
     763called ``Command`` that extends ``django.core.management.base.BaseCommand``. 
     764 
     765For more details on how to define your own commands, look at the code for the 
     766existing ``django-admin.py`` commands, in ``/django/core/management/commands``.