问题
I am using Click under a virtualenv and use the entry_point
directive in setuptools to map the root to a function called dispatch.
My tool exposes two subcommands serve
and config
, I am using an option on the top level group to ensure that the user always passes a --path
directive. However the usage turns out as follows:
mycommand --path=/tmp serve
both the serve
and config
sub commands need to ensure that the user always passes a path in and ideally I would like to present the cli as:
mycommand serve /tmp` or `mycommand config validate /tmp
current Click based implemenation is as follows:
# cli root
@click.group()
@click.option('--path', type=click.Path(writable=True))
@click.version_option(__version__)
@click.pass_context
def dispatch(ctx, path):
"""My project description"""
ctx.obj = Project(path="config.yaml")
# serve
@dispatch.command()
@pass_project
def serve(project):
"""Starts WSGI server using the configuration"""
print "hello"
# config
@dispatch.group()
@pass_project
def config(project):
"""Validate or initalise a configuration file"""
pass
@config.command("validate")
@pass_project
def config_validate(project):
"""Reports on the validity of a configuration file"""
pass
@config.command("init")
@pass_project
def config_init(project):
"""Initialises a skeleton configuration file"""
pass
Is this possible without adding the path argument to each sub command?
回答1:
If there is a specific argument that you would like to decorate only onto the group, but be applicable to all commands as needed, you can do that with a bit of extra plumbing like:
Custom Class:
import click
class GroupArgForCommands(click.Group):
"""Add special argument on group to front of command list"""
def __init__(self, *args, **kwargs):
super(GroupArgForCommands, self).__init__(*args, **kwargs)
cls = GroupArgForCommands.CommandArgument
# gather the special arguments
self._cmd_args = {
a.name: a for a in self.params if isinstance(a, cls)}
# strip out the special arguments
self.params = [a for a in self.params if not isinstance(a, cls)]
# hook the original add_command method
self._orig_add_command = click.Group.add_command.__get__(self)
class CommandArgument(click.Argument):
"""class to allow us to find our special arguments"""
@staticmethod
def command_argument(*param_decls, **attrs):
"""turn argument type into type we can find later"""
assert 'cls' not in attrs, "Not designed for custom arguments"
attrs['cls'] = GroupArgForCommands.CommandArgument
def decorator(f):
click.argument(*param_decls, **attrs)(f)
return f
return decorator
def add_command(self, cmd, name=None):
# hook add_command for any sub groups
if hasattr(cmd, 'add_command'):
cmd._orig_add_command = cmd.add_command
cmd.add_command = GroupArgForCommands.add_command.__get__(cmd)
cmd.cmd_args = self._cmd_args
# call original add_command
self._orig_add_command(cmd, name)
# if this command's callback has desired parameters add them
import inspect
args = inspect.signature(cmd.callback)
for arg_name in reversed(list(args.parameters)):
if arg_name in self._cmd_args:
cmd.params[:] = [self._cmd_args[arg_name]] + cmd.params
Using the Custom Class:
To use the custom class, pass the cls
parameter to the click.group()
decorator, use the @GroupArgForCommands.command_argument
decorator for special arguments, and then add a parameter of the same name as the special argument to any commands as needed.
@click.group(cls=GroupArgForCommands)
@GroupArgForCommands.command_argument('special')
def a_group():
"""My project description"""
@a_group.command()
def a_command(special):
"""a command under the group"""
How does this work?
This works because click
is a well designed OO framework. The @click.group()
decorator usually instantiates a click.Group
object but allows this behavior to be over ridden with the cls
parameter. So it is a relatively easy matter to inherit from click.Group
in our own class and over ride desired methods.
In this case we over ride click.Group.add_command()
so that when a command is added we can examine the command callback parameters to see if they have the same name as any of our special arguments. If they match, the argument is added to the command's arguments just as if it had been decorated directly.
In addition GroupArgForCommands
implements a command_argument()
method. This method is used as a decorator when adding a special argument instead of using click.argument()
Test Code:
def process_path_to_project(ctx, cmd, value):
"""param callback example to convert path to project"""
# Use 'path' to construct a project.
# For this example we will just annotate and pass through
return 'converted {}'.format(value)
@click.group(cls=GroupArgForCommands)
@GroupArgForCommands.command_argument('path',
callback=process_path_to_project)
def dispatch():
"""My project description"""
@dispatch.command()
def serve(path):
"""Starts WSGI server using the configuration"""
click.echo('serve {}'.format(path))
@dispatch.group()
def config():
"""Validate or initalise a configuration file"""
pass
@config.command("validate")
def config_validate():
"""Reports on the validity of a configuration file"""
click.echo('config_validate')
@config.command("init")
def config_init(path):
"""Initialises a skeleton configuration file"""
click.echo('config_init {}'.format(path))
if __name__ == "__main__":
commands = (
'config init a_path',
'config init',
'config validate a_path',
'config validate',
'config a_path',
'config',
'serve a_path',
'serve',
'config init --help',
'config validate --help',
'',
)
import sys, time
time.sleep(1)
print('Click Version: {}'.format(click.__version__))
print('Python Version: {}'.format(sys.version))
for command in commands:
try:
time.sleep(0.1)
print('-----------')
print('> ' + command)
time.sleep(0.1)
dispatch(command.split())
except BaseException as exc:
if str(exc) != '0' and \
not isinstance(exc, (click.ClickException, SystemExit)):
raise
Results:
Click Version: 6.7
Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct 3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)]
-----------
> config init a_path
config_init converted a_path
-----------
> config init
Usage: test.py config init [OPTIONS] PATH
Error: Missing argument "path".
-----------
> config validate a_path
Usage: test.py config validate [OPTIONS]
Error: Got unexpected extra argument (a_path)
-----------
> config validate
config_validate
-----------
> config a_path
Usage: test.py config [OPTIONS] COMMAND [ARGS]...
Error: No such command "a_path".
-----------
> config
Usage: test.py config [OPTIONS] COMMAND [ARGS]...
Validate or initalise a configuration file
Options:
--help Show this message and exit.
Commands:
init Initialises a skeleton configuration file
validate Reports on the validity of a configuration...
-----------
> serve a_path
serve converted a_path
-----------
> serve
Usage: test.py serve [OPTIONS] PATH
Error: Missing argument "path".
-----------
> config init --help
Usage: test.py config init [OPTIONS] PATH
Initialises a skeleton configuration file
Options:
--help Show this message and exit.
-----------
> config validate --help
Usage: test.py config validate [OPTIONS]
Reports on the validity of a configuration file
Options:
--help Show this message and exit.
-----------
>
Usage: test.py [OPTIONS] COMMAND [ARGS]...
My project description
Options:
--help Show this message and exit.
Commands:
config Validate or initalise a configuration file
serve Starts WSGI server using the configuration
来源:https://stackoverflow.com/questions/32493912/is-it-possible-to-add-a-global-argument-for-all-subcommands-in-click-based-inter