Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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
  • Loading branch information...
commit 302eeaf1904a2f3852b223a563617d4999b4e9ae 1 parent 2570954
@freakboy3742 freakboy3742 authored
View
159 django/core/management/__init__.py
@@ -1,18 +1,100 @@
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 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.
"""
- Given a command name, returns the Command class instance. Raises
- ImportError if it doesn't exist.
+ 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):
"""
- # Let the ImportError propogate.
- return getattr(__import__('django.core.management.commands.%s' % name, {}, {}, ['Command']), 'Command')()
+ 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):
"""
@@ -25,8 +107,22 @@ def call_command(name, *args, **options):
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 @@ class ManagementUtility(object):
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 @@ def main_help_text(self):
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 @@ def fetch_command(self, subcommand):
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 @@ def execute(self):
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 @@ class ProjectManagementUtility(ManagementUtility):
"""
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
View
16 django/core/management/base.py
@@ -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 print_help(self, prog_name, subcommand):
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):
View
29 docs/django-admin.txt
@@ -735,3 +735,32 @@ distribution. It enables tab-completion of ``django-admin.py`` and
* 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``.
View
0  tests/modeltests/user_commands/__init__.py
No changes.
View
0  tests/modeltests/user_commands/management/__init__.py
No changes.
View
0  tests/modeltests/user_commands/management/commands/__init__.py
No changes.
View
9 tests/modeltests/user_commands/management/commands/dance.py
@@ -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
30 tests/modeltests/user_commands/models.py
@@ -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'
+
+
+"""}
Please sign in to comment.
Something went wrong with that request. Please try again.