I am having the following problem and I am fearful there isn't a straghtforward way to solve it so I am asking here. I am using Click to implement a CLI and I have created several grouped commands under the main command. This is the code:
#click.group()
def main():
pass
#main.command()
def getq():
'''Parameters: --questionnaire_id, --question_id, --session_id, --option_id'''
click.echo('Question Answers')
When I type the main command alone in my terminal it lists all the subcommands with the help text next to each one. However, the text is not displayed fully for the case of getq. Instead, it displays only "Parameters: --questionnaire_id, --question_id,... ."
Is there a way to display it all?
Thank You
The easiest way to do this is to use the command's short_help argument:
#click.group()
def main():
pass
#main.command(short_help='Parameters: --questionnaire_id, --question_id, --session_id, --option_id')
def getq():
click.echo('Question Answers')
If you insist to use the docstring for this and want to override the automatic shortening of it, then you could use a custom Group class overriding the format_commands method to directly use cmd.help instead of the get_short_help_str method:
import click
from gettext import gettext as _
class FullHelpGroup(click.Group):
def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
"""Extra format methods for multi methods that adds all the commands
after the options.
"""
commands = []
for subcommand in self.list_commands(ctx):
cmd = self.get_command(ctx, subcommand)
# What is this, the tool lied about a command. Ignore it
if cmd is None:
continue
if cmd.hidden:
continue
commands.append((subcommand, cmd))
# allow for 3 times the default spacing
if len(commands):
limit = formatter.width - 6 - max(len(cmd[0]) for cmd in commands)
rows = []
for subcommand, cmd in commands:
help = cmd.help if cmd.help is not None else ""
rows.append((subcommand, help))
if rows:
with formatter.section(_("Commands")):
formatter.write_dl(rows)
#click.group(cls=FullHelpGroup)
def main():
pass
#main.command()
def getq():
'''Parameters: --questionnaire_id, --question_id, --session_id, --option_id'''
click.echo('Question Answers')
if __name__ == "__main__":
main()
You most probably want to override the max_content_width (at most 80 columns by default) also. You could do this by overriding the context settings:
import shutil
#click.group(cls=FullHelpGroup,
context_settings={'max_content_width': shutil.get_terminal_size().columns - 10})
def main():
pass
Related
I'm using click to write a cli program in Python, and I need to write something like this:
import click
#click.group()
def root():
"""root"""
pass
#root.group()
def cli():
"""test"""
pass
#root.group()
def cli2():
"""test"""
pass
#cli.command('test1')
#cli2.command('test1')
def test1():
"""test2"""
print 1234
return
root()
but this will fail with:
TypeError: Attempted to convert a callback into a command twice.
How can I share the command between multiple groups?
The group.command() decorator is a short cut which performs two functions. One is to create a command, the other is to attach the command to a group.
So, to share a command with multiple groups, you can decorate the command for one group like:
#cli.command('test1')
Then, since the command has already been created, you can simply add the click command object to other groups like:
cli2.add_command(test1)
Test Code:
import click
#click.group()
def root():
"""root"""
pass
#root.group()
def cli1():
click.echo('cli1')
#root.group()
def cli2():
click.echo('cli2')
#cli1.command('test1')
#click.argument('arg1')
def test1(arg1):
click.echo('test1: %s' % arg1)
cli2.add_command(test1)
root('cli2 test1 an_arg'.split())
Results:
cli2
test1: an_arg
import click
#click.group()
#click.option('--username')
def cli1(username):
click.echo(username)
#cli1.command()
def something():
click.echo('howdy')
#click.group()
def cli2():
pass
#cli2.command()
def somethingelse():
click.echo('doody')
cli = click.CommandCollection(sources=[cli1, cli2])
if __name__ == '__main__':
cli()
I would expect that this would allow me to pass --username to something, but when I run this script:
python script.py something --username hi
I get:
Error: no such option: --username
It seems like using the CommandCollection is breaking my options. Has anyone else dealt with this before? There is an open ticket in the click repo for this that hasn't been touched since 2015 and has no solution.
With a bit of new plumbing this can be done.
How??
You can inherit from click.Group and then pass the created class to click.group() like:
#click.group(cls=GroupWithCommandOptions)
In the new class, the options on the group can be applied to the command for parsing, and then during command invocation, the group function can be called with the appropriate options.
New Group Class:
import click
class GroupWithCommandOptions(click.Group):
""" Allow application of options to group with multi command """
def add_command(self, cmd, name=None):
""" Hook the added command and put the group options on the command """
click.Group.add_command(self, cmd, name=name)
# add the group parameters to the command
for param in self.params:
cmd.params.append(param)
# hook the command's invoke with our own
cmd.invoke = self.build_command_invoke(cmd.invoke)
self.invoke_without_command = True
def build_command_invoke(self, original_invoke):
def command_invoke(ctx):
""" insert invocation of group function """
# separate the group parameters
ctx.obj = dict(_params=dict())
for param in self.params:
name = param.name
ctx.obj['_params'][name] = ctx.params[name]
del ctx.params[name]
# call the group function with its parameters
params = ctx.params
ctx.params = ctx.obj['_params']
self.invoke(ctx)
ctx.params = params
# now call (invoke) the original command
original_invoke(ctx)
return command_invoke
Test Code:
# Pass new group class to our group which needs options
#click.group(cls=GroupWithCommandOptions)
#click.option('--username')
def cli1(username):
click.echo(username)
#cli1.command()
def something():
click.echo('howdy')
#click.group()
def cli2():
pass
#cli2.command()
def somethingelse():
click.echo('doody')
cli = click.CommandCollection(sources=[cli1, cli2])
if __name__ == '__main__':
cli('something --username hi'.split())
Results:
hi
howdy
I use the excellent Python Click library for handling command line options in my tool. Here's a simplified version of my code (full script here):
#click.command(
context_settings = dict( help_option_names = ['-h', '--help'] )
)
#click.argument('analysis_dir',
type = click.Path(exists=True),
nargs = -1,
required = True,
metavar = "<analysis directory>"
)
def mytool(analysis_dir):
""" Do stuff """
if __name__ == "__main__":
mytool()
If someone runs the command without any flags, they get the default click error message:
$ mytool
Usage: mytool [OPTIONS] <analysis directory>
Error: Missing argument "analysis_dir".
This is nice, but I'd quite like to tell (very) novice users that more help is available by using the help flag. In other words, add a custom sentence to the error message when the command is invalid telling people to try mytool --help for more information.
Is there an easy way to do this? I know I could remove the required attribute and handle this logic in the main function, but that feels kind of hacky for such a minor addition.
Message construction for most errors in python-click is handled by the show method of the UsageError class: click.exceptions.UsageError.show.
So, if you redefine this method, you will be able to create your own customized error message. Below is an example of a customization which appends the help menu to any error message which answers this SO question:
def modify_usage_error(main_command):
'''
a method to append the help menu to an usage error
:param main_command: top-level group or command object constructed by click wrapper
:return: None
'''
from click._compat import get_text_stderr
from click.utils import echo
def show(self, file=None):
import sys
if file is None:
file = get_text_stderr()
color = None
if self.ctx is not None:
color = self.ctx.color
echo(self.ctx.get_usage() + '\n', file=file, color=color)
echo('Error: %s\n' % self.format_message(), file=file, color=color)
sys.argv = [sys.argv[0]]
main_command()
click.exceptions.UsageError.show = show
Once you define your main command, you can then run the modifier script:
import click
#click.group()
def cli():
pass
modify_usage_error(cli)
I have not explored whether there are runtime invocations of ClickException other than usage errors. If there are, then you might need to modify your custom error handler to first check that ctx is an attribute before you add the line click.exceptions.ClickException.show = show since it does not appear that ClickException is fed ctx at initialization.
I want to parse some command line arguments with Python's Click library and save the provided values in an object.
My first guess would be to do it like this:
import click
class Configuration(object):
def __init__(self):
# configuration variables
self.MyOption = None
# method call
self.parseCommandlineArguments()
#click.command()
#click.option('--myoption', type=click.INT, default=5)
def parseCommandlineArguments(self, myoption):
# save option's value in the object
self.MyOption = myoption
# create an instance
configuration = Configuration()
print(configuration.MyOption)
However, this does not work, instead I get:
TypeError: parseCommandlineArguments() takes exactly 2 arguments (1 given)
Apparently, passing self to the decorated function is not the correct way to do it. If I remove self from the method arguments then I can e.g. do print(myoption) and it will print 5 on the screen but the value will not be known to any instances of my Configuration() class.
What is the correct way to handle this? I assume it has something to do with context handling in Click but I cannot get it working based on the provided examples.
If I'm understanding you correctly, you want a command line tool that will take configuration options and then do something with those options. If this is your objective then have a look at the example I posted. This example uses command groups and passes a context object through each command. Click has awesome documentation, be sure to read it.
import click
import json
class Configuration(object):
"""
Having a custom context class is usually not needed.
See the complex application documentation:
http://click.pocoo.org/5/complex/
"""
my_option = None
number = None
is_awesome = False
uber_var = 900
def make_conf(self):
self.uber_var = self.my_option * self.number
pass_context = click.make_pass_decorator(Configuration, ensure=True)
#click.group(chain=True)
#click.option('-m', '--myoption', type=click.INT, default=5)
#click.option('-n', '--number', type=click.INT, default=0)
#click.option('-a', '--is-awesome', is_flag=True)
#pass_context
def cli(ctx, myoption, number, is_awesome):
"""
this is where I will save the configuration
and do whatever processing that is required
"""
ctx.my_option = myoption
ctx.number = number
ctx.is_awesome = is_awesome
ctx.make_conf()
pass
#click.command('save')
#click.argument('output', type=click.File('wb'))
#pass_context
def save(ctx, output):
"""save the configuration to a file"""
json.dump(ctx.__dict__, output, indent=4, sort_keys=True)
return click.secho('configuration saved', fg='green')
#click.command('show')
#pass_context
def show(ctx):
"""print the configuration to stdout"""
return click.echo(json.dumps(ctx.__dict__, indent=4, sort_keys=True))
cli.add_command(save)
cli.add_command(show)
After this is installed your can run commands like this:
mycli -m 30 -n 40 -a show
mycli -m 30 -n 40 -a save foo.json
mycli -m 30 -n 40 -a show save foo.json
The complex example is an excellent demo for developing a highly configurable multi chaining command line tool.
Everyone at Class too big and hard to add new features is completely unphased by the question, which somehow connects command line options to methods, but I can find no documentation for this. It's not optparse, or argparse, or sys.argv - the question implies some kind of direct relationship between methods and command line options. What am I missing?
There isn't any set-in-stone link between them. The question you link to appears to be a program that can do one of several different things, with command-line arguments switching between them. These things happen to be implemented in the program using methods.
It is implied by the question that they have used something like argparse to write the glue between these; but the use of methods is just an implementation detail of the particular program.
I simply use the class like this, what seems not to be a very good idea, because it is very hard to maintain once u got many commands.
class myprogram(object):
def __init__(self)
self.prepare()
def prepare(self):
# some initializations
self.prepareCommands()
def prepareCommands(self):
self.initCommand("--updateDatabase", self.updateDatabase)
self.initCommand("--getImages", self.getImages)
# and so on
def initCommand(self, cmd, func):
options = sys.argv
for option in options:
if option.find(cmd)!=-1:
return func()
# my commands
def updateDatabase(self):
#...
def getImages(self):
#...
if __name__ == "__main__":
p = myprogram()
EDIT1:
Here a cleaner way I just implemented:
myprogram.py:
from config import * # has settings
from commands import *
from logsys import log
import filesys
class myprogram(object):
def __init__(self):
log(_class=self.__name__, _func='__init__', _level=0)
log(_class=self.__name__, _func='__init__', text="DEBUG LEVEL %s" % settings["debug"], _level=0)
self.settings = settings
self.cmds = commands
def prepare(self):
log(_class=self.__name__, _func='prepare', _level=1)
self.dirs = {}
for key in settings["dir"].keys():
self.dirs[key] = settings["dir"][key]
filesys.checkDir(self.dirs[key])
def initCommands(self):
log(_class=self.__name__, _func='initCommands', _level=1)
options = sys.argv
for option in options:
for cmd in self.cmds.keys():
if option.find(cmd) != -1:
return self.cmds[cmd]()
if __name__ == '__main__':
p = myprogram()
p.prepare()
p.initCommands()
commands.py:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
commands = {}
#csv
import csvsys
commands["--getCSV"] = csvsys.getCSV
#commands["--getCSVSplitted"] = csvsys.getCSVSplitted
# update & insert
import database
commands["--insertProductSpecification"] = database.insertProductSpecification
# download
import download
commands["--downloadProductSites"] = download.downloadProductSites
commands["--downloadImages"] = download.downloadImages
# parse
import parse
commands["--parseProductSites"] = parse.parseProductSites
EDIT2: I have now updated my question you linked to your question with a more complete example Class too big and hard to add new features