Skip to content

Commit

Permalink
Add support for command groups
Browse files Browse the repository at this point in the history
  • Loading branch information
GaretJax committed Sep 14, 2015
1 parent 9571071 commit 5962b69
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 12 deletions.
1 change: 1 addition & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ History
0.2.0 – Unreleased
==================

* Support for command groups
* Added a ``pass_verbosity`` decorator
* Improved test suite

Expand Down
73 changes: 61 additions & 12 deletions djclick/adapter.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
import sys
from functools import update_wrapper

import six

import click

from django import get_version
from django.core.management import CommandError


class ParserAdapter(object):
def parse_args(self, args):
return (self, None)


class CommandAdapter(click.Command):
class DjangoCommandMixin(object):
use_argparse = False
option_list = []

def invoke(self, ctx):
try:
return super(DjangoCommandMixin, self).invoke(ctx)
except CommandError as e:
# Honor the --traceback flag
if ctx.traceback:
raise
click.echo('{}: {}'.format(e.__class__.__name__, e), err=True)
ctx.exit(1)

def run_from_argv(self, argv):
"""
Called when run from the command line.
Expand Down Expand Up @@ -58,6 +70,25 @@ def execute(self, *args, **kwargs):
# Invoke the command
self.invoke(ctx)

def __call__(self, *args, **kwargs):
"""
When invoked, normal click commands act as entry points for command
line execution. When using Django, commands get invoked either through
the `execute_from_command_line` or `call_command` utilities.
Calling a command directly can thus be just a shortcut for calling its
`execute` method.
"""
return self.execute(*args, **kwargs)


class CommandAdapter(DjangoCommandMixin, click.Command):
pass


class GroupAdapter(DjangoCommandMixin, click.Group):
pass


def register_on_context(ctx, param, value):
setattr(ctx, param.name, value)
Expand All @@ -72,13 +103,14 @@ def suppress_colors(ctx, param, value):
return value


class CommandRegistrator(object):
class BaseRegistrator(object):
common_options = [
click.option(
'-v', '--verbosity',
expose_value=False,
default='1',
callback=register_on_context,
type=click.Choice(str(s) for s in range(4)),
type=click.IntRange(min=0, max=3),
help=('Verbosity level; 0=minimal output, 1=normal ''output, '
'2=verbose output, 3=very verbose output.'),
),
Expand All @@ -98,8 +130,9 @@ class CommandRegistrator(object):
'"/home/djangoprojects/myproject".'),
),
click.option(
'--traceback',
'--traceback/--no-traceback',
is_flag=True,
default=False,
expose_value=False,
callback=register_on_context,
help='Raise on CommandError exceptions.',
Expand All @@ -109,7 +142,8 @@ class CommandRegistrator(object):
default=None,
expose_value=False,
callback=suppress_colors,
help='Do not colorize the command output.',
help=('Enable or disable output colorization. Default is to '
'autodetect the best behavior.'),
),
]

Expand Down Expand Up @@ -142,18 +176,33 @@ def __call__(self, func):

# Build the click command
decorators = [
click.command(name=self.name, cls=CommandAdapter, **self.kwargs),
click.command(name=self.name, cls=self.cls, **self.kwargs),
] + self.get_params(self.name)

command = func
for decorator in reversed(decorators):
command = decorator(command)
func = decorator(func)

# Django expects the command to be callable (it instantiates the class
# pointed at by the `Command` module-level property)...
# ...let's make it happy.
module.Command = lambda: command
module.Command = lambda: func

return func


def pass_verbosity(f):
"""
Marks a callback as wanting to receive the verbosity as a keyword argument.
"""
def new_func(*args, **kwargs):
kwargs['verbosity'] = click.get_current_context().verbosity
return f(*args, **kwargs)
return update_wrapper(new_func, f)


class CommandRegistrator(BaseRegistrator):
cls = CommandAdapter


# Return the execute method, as this allows us to call the command
# directly (similarly as with `call_command`)
return command.execute
class GroupRegistrator(BaseRegistrator):
cls = GroupAdapter
13 changes: 13 additions & 0 deletions djclick/test/test_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,16 @@ def test_django_version(manage):
assert manage('testcmd', '--version') == prefix + django_version
assert manage('versioncmd', '--version') == prefix + b'20.0\n'


def test_group_command(capsys):
execute_from_command_line(['./manage.py', 'groupcmd'])
out, err = capsys.readouterr()
assert out == 'group_command\n'

execute_from_command_line(['./manage.py', 'groupcmd', 'subcmd1'])
out, err = capsys.readouterr()
assert out == 'group_command\nSUB1\n'

execute_from_command_line(['./manage.py', 'groupcmd', 'subcmd3'])
out, err = capsys.readouterr()
assert out == 'group_command\nSUB2\n'
16 changes: 16 additions & 0 deletions djclick/test/testprj/testapp/management/commands/groupcmd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import djclick as click


@click.group(invoke_without_command=True)
def main():
click.echo('group_command')


@main.command()
def subcmd1():
click.echo('SUB1')


@main.command(name='subcmd3')
def subcmd2():
click.echo('SUB2')

0 comments on commit 5962b69

Please sign in to comment.