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.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@6400 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Russell Keith-Magee 2007-09-21 16:19:20 +00:00
parent 2570954a9a
commit 302eeaf190
8 changed files with 203 additions and 40 deletions

View File

@ -1,18 +1,100 @@
import django import django
from django.core.management.base import BaseCommand, CommandError, handle_default_options
from optparse import OptionParser from optparse import OptionParser
import os import os
import sys import sys
from imp import find_module
# For backwards compatibility: get_version() used to be in this module. # For backwards compatibility: get_version() used to be in this module.
get_version = django.get_version 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 Given a path to a management directory, return a list of all the command names
ImportError if it doesn't exist. that are available. Returns an empty list if no commands are defined.
""" """
# Let the ImportError propogate. command_dir = os.path.join(management_dir,'commands')
return getattr(__import__('django.core.management.commands.%s' % name, {}, {}, ['Command']), 'Command')() 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): def call_command(name, *args, **options):
""" """
@ -25,8 +107,22 @@ def call_command(name, *args, **options):
call_command('shell', plain=True) call_command('shell', plain=True)
call_command('sqlall', 'myapp') 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) 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): class ManagementUtility(object):
""" """
@ -38,21 +134,9 @@ class ManagementUtility(object):
def __init__(self, argv=None): def __init__(self, argv=None):
self.argv = argv or sys.argv[:] self.argv = argv or sys.argv[:]
self.prog_name = os.path.basename(self.argv[0]) self.prog_name = os.path.basename(self.argv[0])
self.commands = self.default_commands() self.project_directory = None
self.user_commands = False
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])
def main_help_text(self): def main_help_text(self):
""" """
Returns the script's main help text, as a string. Returns the script's main help text, as a string.
@ -61,7 +145,7 @@ class ManagementUtility(object):
usage.append('Django command line tool, version %s' % django.get_version()) 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("Type '%s help <subcommand>' for help on a specific subcommand." % self.prog_name)
usage.append('Available subcommands:') usage.append('Available subcommands:')
commands = self.commands.keys() commands = get_commands(self.user_commands, self.project_directory).keys()
commands.sort() commands.sort()
for cmd in commands: for cmd in commands:
usage.append(' %s' % cmd) usage.append(' %s' % cmd)
@ -74,16 +158,26 @@ class ManagementUtility(object):
django-admin.py or manage.py) if it can't be found. django-admin.py or manage.py) if it can't be found.
""" """
try: 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: except KeyError:
sys.stderr.write("Unknown command: %r\nType '%s help' for usage.\n" % (subcommand, self.prog_name)) sys.stderr.write("Unknown command: %r\nType '%s help' for usage.\n" % (subcommand, self.prog_name))
sys.exit(1) sys.exit(1)
return klass
def execute(self): def execute(self):
""" """
Given the command-line arguments, this figures out which subcommand is Given the command-line arguments, this figures out which subcommand is
being run, creates a parser appropriate to that command, and runs it. 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: try:
subcommand = self.argv[1] subcommand = self.argv[1]
except IndexError: except IndexError:
@ -91,8 +185,8 @@ class ManagementUtility(object):
sys.exit(1) sys.exit(1)
if subcommand == 'help': if subcommand == 'help':
if len(self.argv) > 2: if len(args) > 2:
self.fetch_command(self.argv[2]).print_help(self.prog_name, self.argv[2]) self.fetch_command(args[2]).print_help(self.prog_name, args[2])
else: else:
sys.stderr.write(self.main_help_text() + '\n') sys.stderr.write(self.main_help_text() + '\n')
sys.exit(1) sys.exit(1)
@ -116,16 +210,9 @@ class ProjectManagementUtility(ManagementUtility):
""" """
def __init__(self, argv, project_directory): def __init__(self, argv, project_directory):
super(ProjectManagementUtility, self).__init__(argv) super(ProjectManagementUtility, self).__init__(argv)
self.project_directory = project_directory
# Remove the "startproject" command from self.commands, because self.user_commands = True
# 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)
def setup_environ(settings_mod): def setup_environ(settings_mod):
""" """
Configure the runtime environment. This can also be used by external Configure the runtime environment. This can also be used by external

View File

@ -9,6 +9,17 @@ import os
class CommandError(Exception): class CommandError(Exception):
pass 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): class BaseCommand(object):
# Metadata about this command. # Metadata about this command.
option_list = ( option_list = (
@ -55,10 +66,7 @@ class BaseCommand(object):
def run_from_argv(self, argv): def run_from_argv(self, argv):
parser = self.create_parser(argv[0], argv[1]) parser = self.create_parser(argv[0], argv[1])
options, args = parser.parse_args(argv[2:]) options, args = parser.parse_args(argv[2:])
if options.settings: handle_default_options(options)
os.environ['DJANGO_SETTINGS_MODULE'] = options.settings
if options.pythonpath:
sys.path.insert(0, options.pythonpath)
self.execute(*args, **options.__dict__) self.execute(*args, **options.__dict__)
def execute(self, *args, **options): def execute(self, *args, **options):

View File

@ -735,3 +735,32 @@ distribution. It enables tab-completion of ``django-admin.py`` and
* Press [TAB] to see all available options. * Press [TAB] to see all available options.
* Type ``sql``, then [TAB], to see all available options whose names start * Type ``sql``, then [TAB], to see all available options whose names start
with ``sql``. 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``.

View File

@ -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."

View File

@ -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'
"""}