23👍
Django 2.1 and above
In Django 2.1 and above, adding a subcommand is trivial:
from django.core.management.base import BaseCommand
class Command(BaseCommand):
def add_arguments(self, parser):
subparsers = parser.add_subparsers(title="subcommands",
dest="subcommand",
required=True)
Then you use subparser
the same way you’d do if you were writing a non-Django application that uses argparse
. For instance, if you want a subcommand named foo
that may take the --bar
argument:
foo = subparsers.add_parser("foo")
foo.set_defaults(subcommand=fooVal)
foo.add_argument("--bar")
The value fooVal
is whatever you decide the subcommand
option should be set to when the user specifies the foo
subcommand. I often set it to a callable.
Older versions of Django
It is possible but it requires a bit of work:
from django.core.management.base import BaseCommand, CommandParser
class Command(BaseCommand):
[...]
def add_arguments(self, parser):
cmd = self
class SubParser(CommandParser):
def __init__(self, **kwargs):
super(SubParser, self).__init__(cmd, **kwargs)
subparsers = parser.add_subparsers(title="subcommands",
dest="subcommand",
required=True,
parser_class=SubParser)
When you call add_subparsers
by default argparse
creates a new parser that is of the same class as the parser on which you called add_subparser
. It so happens that the parser you get in parser
is a CommandParser
instance (defined in django.core.management.base). The CommandParser
class requires a cmd
argument before the **kwargs
(whereas the default parser class provided by argparse
only takes **kwargs
):
def __init__(self, cmd, **kwargs):
So when you try to add the subparser, it fails because the constructor is called only with **kwargs
and the cmd
argument is missing.
The code above fixes the issue by passing in parser_class
argument a class that adds the missing parameter.
Things to consider:
-
In the code above, I create a new class because the name
parser_class
suggests that what should be passed there is a real class. However, this also works:def add_arguments(self, parser): cmd = self subparsers = parser.add_subparsers( title="subcommands", dest="subcommand", required=True, parser_class=lambda **kw: CommandParser(cmd, **kw))
Right now I’ve not run into any issues but it is possible that a future change to
argparse
could make using a lambda rather than a real class fail. Since the argument is calledparser_class
and not something likeparser_maker
orparser_manufacture
I would consider such a change to be fair game. -
Couldn’t we just pass one of the stock
argparse
classes rather than pass a custom class inparser_class
? There would be no immediate problem, but there would be unintended consequences. The comments inCommandParser
show that the behavior ofargparse
‘s stick parser is undesirable for Django commands. In particular, the docstring for the class states:""" Customized ArgumentParser class to improve some error messages and prevent SystemExit in several occasions, as SystemExit is unacceptable when a command is called programmatically. """
This is a problem that Jerzyk’s answer suffers from. The solution here avoids that problem by deriving from
CommandParser
and thus providing the correct behavior needed by Django.
3👍
you can add it and it was pretty simple:
class Command(BaseCommand):
help = 'dump/restore/diff'
def add_arguments(self, parser):
parser.add_argument('-s', '--server', metavar='server', type=str,
help='server address')
parser.add_argument('-d', '--debug', help='Print lots of debugging')
subparsers = parser.add_subparsers(metavar='command',
dest='command',
help='sub-command help')
subparsers.required = True
parent_parser = argparse.ArgumentParser(add_help=False)
parent_parser.add_argument('machine', metavar='device', type=str)
parent_parser.add_argument('-e', '--errors', action='store_true')
parser_dump = subparsers.add_parser('dump', parents=[parent_parser],
cmd=self)
parser_dump.add_argument('-i', '--indent', metavar='indent', type=int,
default=None, help='file indentation')
parser_restore = subparsers.add_parser('restore',
parents=[parent_parser],
cmd=self)
parser_restore.add_argument('infile', nargs='?',
type=argparse.FileType('r'),
default=sys.stdin)
parser_diff = subparsers.add_parser('diff', parents=[parent_parser],
cmd=self)
parser_diff.add_argument('infile', nargs='?',
type=argparse.FileType('r'),
default=sys.stdin)