Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Django command that gives a set of subcommands a namespace. For example, a command named after an app can be defined with a set of subcommands, e.g. `python manage.py command subcommand`.

Subcommander

Managment commands are assumed to be unique across all apps in a Django project. This can lead to long or obscure command names in attempt to namespace those commands.

Subcommander acts as a proxy command giving the real commands a namespace. The subcommander module can be named after the app name or some derivation. The structure looks as follows:

myapp/
    management/
        commands/
            myapp.py
        subcommands/
            cmd1.py
            cmd2.py
            ...

The command defined in each module under subcommands/ are defined as normal. The commands/myapp.py should look like the following:

from subcommander import Subcommander

class Command(Subcommander):
    subcommands = {
        'cmd1': 'cmd1',
        'cmd2': 'cmd2',
    }

The subcommands dict looks unnecessary, but it enables mapping a cleaner command line name to the command module, e.g. load => load_data.

import sys
from optparse import NO_DEFAULT, OptionParser
from django.core.management.base import CommandError, BaseCommand, handle_default_options
from django.utils.importlib import import_module
class Subcommander(BaseCommand):
help = "A wrapper for subcommands"
subcommands = {}
import_template = '{app_name}.management.subcommands.{module_name}'
def print_subcommands(self, prog_name):
usage = ['', 'Available subcommands:']
for name in sorted(self.subcommands):
usage.append(' {0}'.format(name))
return '\n'.join(usage)
def usage(self, subcommand):
usage = '%prog {0} subcommand [options] [args]'.format(subcommand)
if self.help:
return '{0}\n\n{1}'.format(usage, self.help)
return usage
def print_help(self, prog_name, subcommand):
super(Subcommander, self).print_help(prog_name, subcommand)
sys.stdout.write('{0}\n\n'.format(self.print_subcommands(prog_name)))
def get_subcommand(self, subcommand):
try:
module = import_module(self.import_template.format(app_name=self.app_name,
module_name=self.subcommands[subcommand]))
return module.Command()
except KeyError:
raise CommandError('Unknown subcommand: {0} {1}'.format(self.app_name, subcommand))
def run_from_argv(self, argv):
"""Set up any environment changes requested (e.g., Python path
and Django settings), then run this command.
"""
if len(argv) > 2 and not argv[2].startswith('-') and argv[2] in self.subcommands.keys():
subcommand = argv[2]
klass = self.get_subcommand(subcommand)
parser = OptionParser(prog=argv[0], usage=klass.usage('{0} {1}'.format(argv[1], subcommand)),
version=klass.get_version(), option_list=klass.option_list)
options, args = parser.parse_args(argv[3:])
args = [subcommand] + args
else:
parser = self.create_parser(argv[0], argv[1])
options, args = parser.parse_args(argv[2:])
handle_default_options(options)
self.execute(*args, **options.__dict__)
def handle(self, *args, **options):
if not args or args[0] not in self.subcommands.keys():
return self.print_help('./manage.py', self.app_name)
subcommand, args = args[0], args[1:]
klass = self.get_subcommand(subcommand)
# Grab out a list of defaults from the options. optparse does this for
# us when the script runs from the command line, but since
# call_command can be called programatically, we need to simulate the
# loading and handling of defaults (see #10080 for details).
defaults = {}
for opt in klass.option_list:
if opt.default is NO_DEFAULT:
defaults[opt.dest] = None
else:
defaults[opt.dest] = opt.default
defaults.update(options)
return klass.execute(*args, **defaults)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment