djclick/adapter.py
import os
import sys
from functools import update_wrapper
import click
from django import get_version, VERSION as DJANGO_VERSION
from django.core.management import CommandError
class OptionParseAdapter(object):
"""Django pre-1.10-compatible adapter, deprecated"""
def parse_args(self, args):
return (self, None) # NOCOV
class ArgumentParserDefaults(object):
def __init__(self, args):
self._args = args
def _get_kwargs(self):
return {
"args": self._args,
}
class ArgumentParserAdapter(object):
def __init__(self):
self._actions = []
self._mutually_exclusive_groups = []
def parse_args(self, args):
return ArgumentParserDefaults(args)
class DjangoCommandMixin(object):
use_argparse = False
option_list = []
base_stealth_options = []
@property
def stealth_options(self):
return sum(
([p.name] + [i.lstrip("-") for i in p.opts] for p in self.params), [],
)
def invoke(self, ctx):
try:
return super(DjangoCommandMixin, self).invoke(ctx)
except CommandError as e:
# Honor the --traceback flag
if ctx.traceback: # NOCOV
raise
styled_message = click.style(
"{}: {}".format(e.__class__.__name__, e), fg="red", bold=True
)
click.echo(styled_message, err=True)
ctx.exit(1)
def run_from_argv(self, argv):
"""
Called when run from the command line.
"""
prog_name = "{} {}".format(os.path.basename(argv[0]), argv[1])
try:
# We won't get an exception here in standalone_mode=False
exit_code = self.main(
args=argv[2:], prog_name=prog_name, standalone_mode=False
)
if exit_code:
sys.exit(exit_code)
except click.ClickException as e:
if getattr(e, "ctx", False) and getattr(e.ctx, "traceback", False): # NOCOV
raise
e.show()
sys.exit(e.exit_code)
def create_parser(self, progname, subcommand):
"""
Called when run through `call_command`.
"""
if DJANGO_VERSION >= (1, 10):
return ArgumentParserAdapter()
else: # NOCOV
return OptionParseAdapter()
def print_help(self, prog_name, subcommand):
prog_name = "{} {}".format(prog_name, subcommand)
self.main(["--help"], prog_name=prog_name, standalone_mode=False)
def map_names(self):
for param in self.params:
for opt in param.opts:
yield opt.lstrip("--").replace("-", "_"), param.name
def execute(self, *args, **kwargs):
"""
Called when run through `call_command`. `args` are passed through,
while `kwargs` is the __dict__ of the return value of
`self.create_parser('', name)` updated with the kwargs passed to
`call_command`.
"""
# Remove internal Django command handling machinery
kwargs.pop("skip_checks", None)
parent_ctx = click.get_current_context(silent=True)
with self.make_context("", list(args), parent=parent_ctx) as ctx:
# Rename kwargs to to the appropriate destination argument name
opt_mapping = dict(self.map_names())
arg_options = {
opt_mapping.get(key, key): value for key, value in kwargs.items()
}
# Update the context with the passed (renamed) kwargs
ctx.params.update(arg_options)
# 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)
return value
def suppress_colors(ctx, param, value):
# Only set the value if a flag was provided, otherwise we would override
# the default set by the parent context if one was available.
if value is not None:
ctx.color = value
return value
class BaseRegistrator(object):
common_options = [
click.option(
"-v",
"--verbosity",
expose_value=False,
default="1",
callback=register_on_context,
type=click.IntRange(min=0, max=3),
help=(
"Verbosity level; 0=minimal output, 1=normal "
"output, "
"2=verbose output, 3=very verbose output."
),
),
click.option(
"--settings",
metavar="SETTINGS",
expose_value=False,
help=(
"The Python path to a settings module, e.g. "
'"myproject.settings.main". If this is not provided, the '
"DJANGO_SETTINGS_MODULE environment variable will be used."
),
),
click.option(
"--pythonpath",
metavar="PYTHONPATH",
expose_value=False,
help=(
"A directory to add to the Python path, e.g. "
'"/home/djangoprojects/myproject".'
),
),
click.option(
"--traceback/--no-traceback",
is_flag=True,
default=False,
expose_value=False,
callback=register_on_context,
help="Raise on CommandError exceptions.",
),
click.option(
"--color/--no-color",
default=None,
expose_value=False,
callback=suppress_colors,
help=(
"Enable or disable output colorization. Default is to "
"autodetect the best behavior."
),
),
]
def __init__(self, **kwargs):
self.kwargs = kwargs
self.version = self.kwargs.pop("version", get_version())
context_settings = kwargs.setdefault("context_settings", {})
context_settings["help_option_names"] = ["-h", "--help"]
def get_params(self, name):
def show_help(ctx, param, value):
if value and not ctx.resilient_parsing:
click.echo(ctx.get_help(), color=ctx.color)
ctx.exit()
return [
click.version_option(version=self.version, message="%(version)s"),
click.option(
"-h",
"--help",
is_flag=True,
is_eager=True,
expose_value=False,
callback=show_help,
help="Show this message and exit.",
),
] + self.common_options
def __call__(self, func):
module = sys.modules[func.__module__]
# Get the command name as Django expects it
self.name = func.__module__.rsplit(".", 1)[-1]
# Build the click command
decorators = [
click.command(name=self.name, cls=self.cls, **self.kwargs),
] + self.get_params(self.name)
for decorator in reversed(decorators):
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: 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
class GroupRegistrator(BaseRegistrator):
cls = GroupAdapter