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:
parent
2570954a9a
commit
302eeaf190
|
@ -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,9 +107,23 @@ 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):
|
||||||
"""
|
"""
|
||||||
Encapsulates the logic of the django-admin.py and manage.py utilities.
|
Encapsulates the logic of the django-admin.py and manage.py utilities.
|
||||||
|
@ -38,20 +134,8 @@ 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):
|
||||||
"""
|
"""
|
||||||
|
@ -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,15 +210,8 @@ 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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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``.
|
||||||
|
|
|
@ -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."
|
|
@ -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'
|
||||||
|
|
||||||
|
|
||||||
|
"""}
|
Loading…
Reference in New Issue