Python Click: How to print full help details on usage error? - python

I'm using python click for my CLI. When I pass in the wrong set of arguments or flags, a usage message pops up. However, when I use the --help flag a more detailed usage message pops up with a list of all options and arguments. Is there a way to change the default behavior so that a usage error prints the full detailed help?
For example, a missing argument prints
mycli foo
Usage: mycli foo [OPTIONS] MY_ARG
Error: Missing argument "my_arg".
But adding --help prints
mycli foo --help
Usage: mycli foo [OPTIONS] MY_ARG
Long and useful description of the command and stuff.
Options:
-h, --help Show this message and exit.
The command is implemented roughly like so
#click.group()
#click.pass_context
def cli(ctx):
ctx.obj = {}
#cli.command()
#click.argument('my_arg')
#click.pass_context
#report_errors
def foo(ctx, my_arg):
# some stuff here

it could be done by monkey-patching UsageError
import click
from click.exceptions import UsageError
from click._compat import get_text_stderr
from click.utils import echo
def _show_usage_error(self, file=None):
if file is None:
file = get_text_stderr()
color = None
if self.ctx is not None:
color = self.ctx.color
echo(self.ctx.get_help() + '\n', file=file, color=color)
echo('Error: %s' % self.format_message(), file=file, color=color)
UsageError.show = _show_usage_error
#click.group()
#click.pass_context
def cli(ctx):
ctx.obj = {}
#cli.command()
#click.argument('my_arg')
#click.pass_context
#report_errors
def foo(ctx, my_arg):
# some stuff here

Related

How would I structure a kubectl-like CLI with python-click?

I would like to create a CLI that receives an action and resource parameters to work, in a kubectl fashion. Eg.
myctl init config
myctl create issue|pr|branch
myctl delete issue|pr|branch|config
myctl command should always receive 2 arguments, so that the user wouldn't try something like: myctl init config delete issue. Also the should not be able to execute impossible combinations such as myctl create config.
I though of some code like:
import click
#click.command()
#click.group()
#click.option(
"init,create,delete",
type=str,
help="Action name.",
)
#click.option(
"config,issue,pr,branch",
type=str,
help="Resource name.",
)
def main(action: str, resource: str) -> None:
pass
if __name__ == "__main__":
main(prog_name="myctl")
I am not really sure what click elements should I use in order to structure that (arguments, options, groups, etc) and how to put that together.
I've achieved this in the past by creating multiple nested groups.
import click
#click.group('cli')
def cli():
pass
#click.group('init')
def init():
pass
#click.group('create')
def create():
pass
#click.group('delete')
def delete():
pass
#init.command('config')
def config():
print('Configuration complete')
#create.command('issue')
#click.argument('name')
def issue(name):
print(f'Created {name}')
#create.command('pr')
#click.argument('base_branch', required=False, default='master')
def pr(base_branch):
print(f'Created PR against {base_branch}')
#create.command('branch')
#click.argument('name')
def branch(name):
print(f'Create branch {name}')
#delete.command('issue')
#click.argument('issue_number')
def delete_issue(issue_number):
print(f'Deleting issue {issue_number}')
#delete.command('pr')
#click.argument('pr_number')
def delete_pr(pr_number):
print(f'Deleting PR {pr_number}')
#delete.command('branch')
#click.argument('branch_name')
def delete_branch(branch_name):
print(f'Deleting branch {branch_name}')
#delete.command('config')
#click.argument('config_name')
def delete_config(config_name):
print(f'Deleting config {config_name}')
cli.add_command(init)
cli.add_command(create)
cli.add_command(delete)
def run_cli():
cli()
if __name__ == "__main__":
run_cli()
You can then expand this however you want, with invocations looking like the following. I've called my CLI play.
❯ play
Usage: play [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
create
delete
init
❯ play init
Usage: play init [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
config
❯ play init config
Configuration complete
❯ play create
Usage: play create [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
branch
issue
pr
❯ play create branch feature/the-coolest
Create branch feature/the-coolest
You can then proceed to add short help messages and customise it to your application.

How to login to an application directly from command line, i.e by providing URL, username, password in the command line of a Pytest

Instead of hard coding the URL of the application under test in the code, I would like to pass it as an argument in the command line. Also, the username, password.
Currently I get below error
ERROR: file not found:
pytest -v -s --html=.\Reports\report.html test_practise.py https://testurl
Below is my Login function:
class LoginPage():
def __init__(self, driver):
self.driver = driver
self.url = 'https://test-url'
def go(self):
self.driver.get(self.url)
def enter_username(self,text):
username_xpath = "//input[#id='emailOrUsername']"
WebDriverWait(self.driver, 20).until(EC.element_to_be_clickable((By.XPATH, username_xpath))).send_keys(text)
def enter_password(self,text):
password_xpath = "//input[#id='pass']"
WebDriverWait(self.driver, 20).until(EC.element_to_be_clickable((By.XPATH, password_xpath))).send_keys(text)
def click_login_button(self):
login_btn_xpath = "//span[contains(text(),'Login')]"
self.driver.find_element_by_xpath(login_btn_xpath).click()
def get_login_btn_text(self):
login_btn_xpath = "//span[contains(text(),'Login')]"
login_btn_text = self.driver.find_element_by_xpath(login_btn_xpath).text
return login_btn_text
There is a built-in module argparse. Check the tutorial by the link.
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("echo")
args = parser.parse_args()
print(args.echo)
And here is a result:
$ python3 prog.py --help
usage: prog.py [-h] echo
positional arguments:
echo
optional arguments:
-h, --help show this help message and exit
$ python3 prog.py foo
foo

Use Python click command to invoke a class method with variadic arguments

I have a class that gets initialized with a previously unknown number of arguments and I want it to be done on CLI using Python's click package. My issue is that I can't manage to initialize it and run a click command:
$ python mycode.py arg1 arg2 ... argN click_command
Setting a defined number of arguments, like nargs=5, solves the issue of missing command but obligates me to input 5 arguments before my command. With variadic arguments like nargs=-1, click doesn't recognize click_command as a command.
How can I input n-many arguments, and then run the command using click?
import click
class Foo(object):
def __init__(self, *args):
self.args = args
def log(self):
print('self.args:', self.args)
pass_foo = click.make_pass_decorator(Foo)
#click.group()
#click.argument('myargs', nargs=-1)
#click.pass_context
def main(ctx, myargs):
ctx.obj = Foo(myargs)
print("arguments: ", myargs)
#main.command()
#pass_foo
def log(foo):
foo.log()
main()
I expect to be able to run a click command after passing n-many args to my Foo() class, so I can initialize it and run its log() method as a CLI command, but the output is:
Error: Missing command
I am not entirely sure what you are trying to do is the best way to approach this problem. I would think that placing the variadic arguments after the command would be a bit more logical, and would definitely more align with the way click works. But, you can do what you are after with this:
Custom Class:
class CommandAfterArgs(click.Group):
def parse_args(self, ctx, args):
parsed_args = super(CommandAfterArgs, self).parse_args(ctx, args)
possible_command = ctx.params['myargs'][-1]
if possible_command in self.commands:
ctx.protected_args = [possible_command]
ctx.params['myargs'] = ctx.params['myargs'][:-1]
elif possible_command in ('-h', '--help'):
if len(ctx.params['myargs']) > 1 and \
ctx.params['myargs'][-2] in self.commands:
ctx.protected_args = [ctx.params['myargs'][-2]]
parsed_args = ['--help']
ctx.params['myargs'] = ctx.params['myargs'][:-2]
ctx.args = [possible_command]
return parsed_args
Using Custom Class:
Then to use the custom class, pass it as the cls argument to the group decorator like:
#click.group(cls=CommandAfterArgs)
#click.argument('myargs', nargs=-1)
def main(myargs):
...
Test Code:
import click
class Foo(object):
def __init__(self, *args):
self.args = args
def log(self):
print('self.args:', self.args)
pass_foo = click.make_pass_decorator(Foo)
#click.group(cls=CommandAfterArgs)
#click.argument('myargs', nargs=-1)
#click.pass_context
def main(ctx, myargs):
ctx.obj = Foo(*myargs)
print("arguments: ", myargs)
#main.command()
#pass_foo
def log(foo):
foo.log()
if __name__ == "__main__":
commands = (
'arg1 arg2 log',
'log --help',
'--help',
)
import sys, time
time.sleep(1)
print('Click Version: {}'.format(click.__version__))
print('Python Version: {}'.format(sys.version))
for cmd in commands:
try:
time.sleep(0.1)
print('-----------')
print('> ' + cmd)
time.sleep(0.1)
main(cmd.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)]
-----------
> arg1 arg2 log
arguments: ('arg1', 'arg2')
self.args: ('arg1', 'arg2')
-----------
> log --help
arguments: ()
Usage: test.py log [OPTIONS]
Options:
--help Show this message and exit.
-----------
> --help
Usage: test.py [OPTIONS] [MYARGS]... COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
log

Instantiate Foo() class on main click group command by running subcomand with Foo() arguments

I want to run a click subcommand with variadic arguments that are going to be used to instantiate a class Foo(*args) on main() group command in order to create an instance of Foo() to be used by its subcommands so that it aligns with the way click works:
$ python foo.py subcommand arg1 arg2 ... argN
This question is based on my initial question and #StephenRauch answer.
import click
class Foo(object):
def __init__(self, *args):
self.args = args
def log(self):
print('self.args:', self.args)
pass_foo = click.make_pass_decorator(Foo)
#click.group()
#click.pass_context
def main(ctx):
magic_to_get_myargs()
ctx.obj = Foo(myargs)
print("main:\n", "ctx.obj.args:", ctx.obj.args)
#main.command()
#click.argument('myargs', nargs=-1)
#pass_foo
def run(foo, myargs):
magic_to_send_myargs()
print("run:\n", 'foo.args:', foo.args)
foo.log()
main()
I expect to initialize Foo class on main group command by running a subcommand and get back its object to use it within subcommand.
Based on #StephenRauch in a similar answer I have managed to find a solution by myself.
Code
import click
class MyGroup(click.Group):
def invoke(self, ctx):
ctx.obj = tuple(ctx.args)
super(MyGroup, self).invoke(ctx)
class Foo(object):
def __init__(self, *args):
self.args = args
def log(self):
print('self.args:', self.args)
pass_foo = click.make_pass_decorator(Foo)
#click.group(cls=MyGroup)
#click.pass_context
def main(ctx):
ctx.obj = Foo(*ctx.obj)
print("main:\n", "ctx.obj.args:", ctx.obj.args)
#main.command()
#pass_foo
#click.argument('myargs', nargs=-1)
def run(foo, myargs):
print("run:\n", 'foo.args:', foo.args)
foo.log()
if __name__ == "__main__":
commands = (
'run arg1 arg2 arg3',
'run --help',
'--help',
)
import sys, time
time.sleep(1)
print('Click Version: {}'.format(click.__version__))
print('Python Version: {}'.format(sys.version))
for cmd in commands:
try:
time.sleep(0.1)
print("\n", '-' * 50)
print('> ' + cmd)
time.sleep(0.1)
main(cmd.split())
except BaseException as exc:
if str(exc) != '0' and \
not isinstance(exc, (click.ClickException, SystemExit)):
raise
Result
Click Version: 7.0
Python Version: 3.7.2 (default, Dec 29 2018, 06:19:36)
[GCC 7.3.0]
--------------------------------------------------
> run arg1 arg2 arg3
main:
ctx.obj.args: ('arg1', 'arg2', 'arg3')
run:
foo.args: ('arg1', 'arg2', 'arg3')
self.args: ('arg1', 'arg2', 'arg3')
--------------------------------------------------
> run --help
main:
ctx.obj.args: ('--help',)
Usage: test3.py run [OPTIONS] [MYARGS]...
Options:
--help Show this message and exit.
--------------------------------------------------
> --help
Usage: test3.py [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
run

A command without name, in Click

I want to have a command line tool with a usage like this:
$ program <arg> does something, no command name required
$ program cut <arg>
$ program eat <arg>
The Click code would look like this:
#click.group()
def main() :
pass
#main.command()
#click.argument('arg')
def noname(arg) :
# does stuff
#main.command()
#click.argument('arg')
def cut(arg) :
# cuts stuff
#main.command()
#click.argument('arg')
def eat(arg) :
# eats stuff
My problem is that with this code, there is always a required command name, ie: I need to run $ program noname arg. But I want to be able to run $ program arg.
There is an option for you, "Group Invocation Without Command
":
#click.group(invoke_without_command=True)
#click.pass_context
def main(ctx):
if not ctx.invoked_subcommand:
print('main stuff')
click-default-group is doing what you are looking for. It is part of the click-contrib collection.
The advantage of that instead of using invoke_without_command is that it passes the options and arguments flawlessly to the default command, something that is not trivial (or even possible) with the built-in functionality.
Example code:
import click
from click_default_group import DefaultGroup
#click.group(cls=DefaultGroup, default='foo', default_if_no_args=True)
def cli():
print("group execution")
#cli.command()
#click.option('--config', default=None)
def foo(config):
click.echo('foo execution')
if config:
click.echo(config)
Then, it's possible to call foo command with its option as:
$ program foo --config bar <-- normal way to call foo
$ program --config bar <-- foo is called and the option is forwarded.
Not possible with vanilla Click.
Your scheme has some challenges because of the ambiguity introduce with the default command. Regardless, here is one way that can be achieved with click. As shown in the test results, the generated help with be less than ideal, but likely OK.
Custom Class:
import click
class DefaultCommandGroup(click.Group):
"""allow a default command for a group"""
def command(self, *args, **kwargs):
default_command = kwargs.pop('default_command', False)
if default_command and not args:
kwargs['name'] = kwargs.get('name', '<>')
decorator = super(
DefaultCommandGroup, self).command(*args, **kwargs)
if default_command:
def new_decorator(f):
cmd = decorator(f)
self.default_command = cmd.name
return cmd
return new_decorator
return decorator
def resolve_command(self, ctx, args):
try:
# test if the command parses
return super(
DefaultCommandGroup, self).resolve_command(ctx, args)
except click.UsageError:
# command did not parse, assume it is the default command
args.insert(0, self.default_command)
return super(
DefaultCommandGroup, self).resolve_command(ctx, args)
Using the Custom Class
To use the custom class, pass the cls parameter to the click.group() decorator. Then pass default_command=True for the command which will be the default.
#click.group(cls=DefaultCommandGroup)
def a_group():
"""My Amazing Group"""
#a_group.command(default_command=True)
def a_command():
"""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.command() so that when a command is added we find the default command. In addition we override click.Group.resolve_command() so that we can insert the default command name if the first resolution is unsuccessful.
Test Code:
#click.group(cls=DefaultCommandGroup)
def main():
pass
#main.command(default_command=True)
#click.argument('arg')
def noname(arg):
""" does stuff """
click.echo('default: {}'.format(arg))
#main.command()
#click.argument('arg')
def cut(arg):
""" cuts stuff """
click.echo('cut: {}'.format(arg))
#main.command()
#click.argument('arg')
def eat(arg):
""" eats stuff """
click.echo('eat: {}'.format(arg))
if __name__ == "__main__":
commands = (
'an_arg',
'cut cut_arg',
'eat eat_arg',
'--help',
'cut --help',
'eat --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)
main(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)]
-----------
> an_arg
default: an_arg
-----------
> cut cut_arg
cut: cut_arg
-----------
> eat eat_arg
eat: eat_arg
-----------
> --help
Usage: test.py [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
<> does stuff
cut cuts stuff
eat eats stuff
-----------
> cut --help
Usage: test.py cut [OPTIONS] ARG
cuts stuff
Options:
--help Show this message and exit.
-----------
> eat --help
Usage: test.py eat [OPTIONS] ARG
eats stuff
Options:
--help Show this message and exit.
-----------
>
Usage: test.py [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
<> does stuff
cut cuts stuff
eat eats stuff
An end-to-end practical example:
$ test_click add -a 1 -b 2
---
add: 3
------------------
$ test_click sub -a 5 -b 1
---
sub: 4
------------------
$ test_click
enter a operation (add or sub): add
enter a number 1: 1
enter a number 2: 2
---
add: 3
------------------
$ test_click
enter a operation (add or sub): sub
enter a number 1: 5
enter a number 2: 1
---
sub: 4
the code:
import click
#click.group(invoke_without_command=True)
#click.pass_context
def mycommands(ctx):
if ctx.invoked_subcommand is None:
manual_mode()
pass
def manual_mode():
tipo = input('enter a operation (add or sub): ')
arg1 = input('enter a number 1: ')
arg2 = input('enter a number 2: ')
if tipo == 'add':
add_f(int(arg1), int(arg2))
elif tipo == 'sub':
sub_f(int(arg1), int(arg2))
else:
print('type not know')
def add_f(arg1,
arg2):
print('add:', arg1 + arg2)
def sub_f(arg1,
arg2):
print('sub:', arg1 - arg2)
#click.option('-a', 'arg1',
type=click.INT)
#click.option('-b', 'arg2',
type=click.INT)
#mycommands.command()
def add(arg1, arg2):
add_f(arg1, arg2)
#click.option('-a', 'arg1',
type=click.INT)
#click.option('-b', 'arg2',
type=click.INT)
#mycommands.command()
def sub(arg1,
arg2):
sub_f(arg1, arg2)
if __name__ == '__main__':
mycommands()

Categories