I have a click.group() defined, with about 10 commands in it. I understand how to use a group to run code before the code in the command, but I also want to run some code AFTER each command is run. Is that possible with click?
You can use the #resultcallback decorator
#click.group()
def cli():
click.echo('Before command')
#cli.resultcallback()
def process_result(result, **kwargs):
click.echo('After command')
#cli.command()
def command():
click.echo('Command')
if __name__ == '__main__':
cli()
Output:
$ python cli.py command
Before command
Command
After command
This worked for me on Click==7.0. Have not tried resultcallback
$ cat check.py
import click
#click.group()
#click.pass_context
def cli(ctx):
print("> Welcome!!")
ctx.call_on_close(_on_close)
def _on_close():
print("> Done!!")
#cli.command()
def hello():
print("Hello")
if __name__ == '__main__':
cli()
Output:
$ python3 check.py hello
> Welcome!!
Hello
> Done!!
Documentation
Related
I'm trying to use Typer package in python.
I wrote this code:
import typer
app = typer.Typer()
#app.command()
def hello(name: str):
print(f"Hello {name}")
And I'm trying to run this with this command:
python main.py hello Patryk
And my terminal is not displaying anything. No errors, no text.
Python version: Python 3.9.13 (pipenv)
System: Macos Ventura 13.1
Terminal: iTerm2 Build 3.4.18 and Macos default terminal
I tried to not use pipenv and install all typer on my machine.
I also tried this piece of code:
import typer
def main(name: str):
print(f"Hello {name}")
if __name__ == "__main__":
typer.run(main)
And it works when I run it with this command:
python main.py
I found solution. To make it work you need to add:
if __name__ == "__main__":
app()
And have at least 2 commands, so your code has to look like this:
import typer
app = typer.Typer()
#app.command()
def hello(name: str):
print(f"Hello {name}")
#app.command()
def goodbye(name: str):
print(f"Goodbye {name}")
if __name__ == "__main__":
app()
You just forgot to add this below:
if __name__ == "__main__":
app()
Then launch the app like this:
python main.py Patryk
because Typer has defined hello() as the default action if the program is executed. So there is no need to add the hello in front in this case.
Unless you add other commands in addition then you will have to specify the command to run at runtime:
python main.py <command-name> ...
I'm using click in Python to create a CLI for my program. I want to execute a function (below represented through the placeholder do_something()) if an exception occurs, but only if a certain argument is passed through the CLI (here: --verbose or -v).
I know that I can access command line input via sys.argv. This is my conceptual minimal working example using this approach:
#filename: my_app.py
import click
#click.group()
#click.option('--verbose', '-v', is_flag=True)
def cli():
pass
#click.command()
def greeting():
print('hello world')
raise Exception
#cli.command()
def spam():
print('SPAM')
raise Exception
def do_something():
print('Doing something...')
if __name__ == '__main__':
try:
main()
except e:
if '--verbose' in sys.argv or '-v' in sys.argv:
do_something()
raise e
To be as exact as I can be: In case of an exception in any of the subcommands, I want the program to do_something() when the --verbose flag is on, but not if it's off. E.g. for the greeting subcommand the CLI should behave like this:
~$ python my_app.py --verbose greeting
hello world
Doing something...
Exception
~$ python my_app.py greeting
hello world
Exception
While the sys.argv solution shown above works, it would be nice if I could refer to the command line option with click's internal name (which is 'verbose' in this case). I.e., I would like to do something like this here:
...
if __name__ == '__main__':
try:
main()
except e:
if 'verbose' in click.argv:
do_something()
raise e
Is this a feature that click provides?
Correct me if I'm wrong, but it seems to me like you'd like to be able to output more information/clean up something in the case of an exception.
There are a few options you could try:
Move the try/except logic inside the greeting method that leverages the verbose flag, and call do_something there
Consider looking at callbacks and see if you can leverage these to do what you want. Without more context into do_something, I can't tell for sure whether it would be a good use case for callbacks.
The closest thing to your question is to have a global scope variable _verbose like in the example below:
import click
global _verbose
_verbose = False
def do_something():
print(f'Doing something')
#click.group()
def cli():
pass
#cli.command()
#click.option('--verbose', '-v', is_flag=True)
def greeting():
global _verbose
_verbose = verbose
if verbose:
print('hello world')
raise Exception("Test")
else:
print('hello')
#cli.command()
#click.option('--count', '-c')
def spam(count):
print(count * 'SPAM')
if __name__ == '__main__':
try:
cli()
except Exception as e:
if _verbose:
do_something()
raise e
I've updated my answer with a working snippet that doesn't rely on globals to do this, but instead leverages Python's pass-by-reference and click's advanced context examples
import click
def do_something():
print(f'Doing something')
#click.group()
#click.option('--verbose', '-v', is_flag=True)
#click.pass_context
def cli(ctx, verbose):
ctx.obj['verbose'] = verbose
pass
#cli.command()
#click.pass_context
def greeting(ctx):
if ctx.obj['verbose']:
print('hello world')
raise Exception("Test")
else:
print('hello')
#cli.command()
#click.option('--count', '-c')
def spam(count):
print(count * 'SPAM')
if __name__ == '__main__':
flags = {}
try:
cli(obj=flags)
except Exception as e:
if flags['verbose']:
do_something()
raise e
I'm a new bee for python currently working on the Click module. So here I have a doubt to providing input for the main cli function only. But I want to provide the input for my all the function one by one. is it possible to click? Thanks for advance.
#click.option('--create', default='sub', help='Create')
#click.command()
def create(create):
click.echo('create called')
os.system('curl http://127.0.0.1:5000/create')
#click.option('--conn', default='in', help='connect to server')
#click.command()
def conn(conn):
click.echo('conn called')
os.system('curl http://127.0.0.1:5000/')
and my setup.py
from setuptools import setup
setup(
name="hello",
version='0.1',
py_modules=['hello'],
install_requires=[
'Click',
],
entry_points='''
[console_scripts]
hello=hello:cli
''',
)
My output expectation
$ hello --conn in
success
hello --create sub
success
Sounds to me like you want different commands based on input provided to your hello cli. For that reason, Click has the useful notion of a group, a collection of commands that can be invoked.
You can reorganize your code as follows:
#click.group()
def cli():
pass
#cli.command()
def create():
click.echo('create called')
os.system('curl http://127.0.0.1:5000/create')
#cli.command()
def conn():
click.echo('conn called')
os.system('curl http://127.0.0.1:5000/')
def main():
value = click.prompt('Select a command to run', type=click.Choice(list(cli.commands.keys()) + ['exit']))
while value != 'exit':
cli.commands[value]()
if __name__ == "__main__":
main()
and the calls would be:
$ hello con
$ hello create
It doesn't seem like you need the options, as you don't change the behaviour of each command based on the option being passed in or not. For more information, please refer to the commands and groups Click documentation
I have the following code:
import click
#click.group()
def cli():
pass
#click.command()
def initdb():
click.echo('Initialized the database')
#click.command()
def dropdb():
click.echo('Dropped the database')
cli.add_command(initdb)
cli.add_command(dropdb)
At the command line I want to be able to do something like the following:
python clicktest.py cli initdb
and have the following happen echo at the terminal:
Initialized the database
Or to enter into the terminal:
python clicktest.py cli dropdb
and have the following happen on the terminal:
Dropped the database
My problem is currently when I do this at the terminal:
python clicktest.py cli initdb
Nothing happens at the terminal, nothing prints when I think something should, namely the 'Initialized Database' echo. What am i doing wrong??
First, you should use it in command line like:
python clicktest.py initdb
python clicktest.py dropdb
And in your clicktest.py file, place these lines at the bottom of your code:
if __name__ == '__main__':
cli()
Unless, your code won't get work.
EDIT:
If you really want to use it in a way python clicktest.py cli initdb, then you have another choice:
#click.group()
def main():
pass
#click.group()
def cli():
pass
#click.command()
def initdb():
click.echo('Initialized the database')
#click.command()
def dropdb():
click.echo('Dropped the database')
cli.add_command(initdb)
cli.add_command(dropdb)
main.add_command(cli)
if __name__ == '__main__':
main()
Or even better (Using decorators instead):
#click.group()
def main():
pass
#main.group()
def cli():
pass
#cli.command()
def initdb():
click.echo('Initialized the database')
#cli.command()
def dropdb():
click.echo('Dropped the database')
if __name__ == '__main__':
main()
I'm using click (http://click.pocoo.org/3/) to create a command line application, but I don't know how to create a shell for this application.
Suppose I'm writing a program called test and I have commands called subtest1 and subtest2
I was able to make it work from terminal like:
$ test subtest1
$ test subtest2
But what I was thinking about is a shell, so I could do:
$ test
>> subtest1
>> subtest2
Is this possible with click?
This is not impossible with click, but there's no built-in support for that either. The first you would have to do is making your group callback invokable without a subcommand by passing invoke_without_command=True into the group decorator (as described here). Then your group callback would have to implement a REPL. Python has the cmd framework for doing this in the standard library. Making the click subcommands available there involves overriding cmd.Cmd.default, like in the code snippet below. Getting all the details right, like help, should be doable in a few lines.
import click
import cmd
class REPL(cmd.Cmd):
def __init__(self, ctx):
cmd.Cmd.__init__(self)
self.ctx = ctx
def default(self, line):
subcommand = cli.commands.get(line)
if subcommand:
self.ctx.invoke(subcommand)
else:
return cmd.Cmd.default(self, line)
#click.group(invoke_without_command=True)
#click.pass_context
def cli(ctx):
if ctx.invoked_subcommand is None:
repl = REPL(ctx)
repl.cmdloop()
#cli.command()
def a():
"""The `a` command prints an 'a'."""
print "a"
#cli.command()
def b():
"""The `b` command prints a 'b'."""
print "b"
if __name__ == "__main__":
cli()
There is now a library called click_repl that does most of the work for you. Thought I'd share my efforts in getting this to work.
The one difficulty is that you have to make a specific command the repl command, but we can repurpose #fpbhb's approach to allow calling that command by default if another one isn't provided.
This is a fully working example that supports all click options, with command history, as well as being able to call commands directly without entering the REPL:
import click
import click_repl
import os
from prompt_toolkit.history import FileHistory
#click.group(invoke_without_command=True)
#click.pass_context
def cli(ctx):
"""Pleasantries CLI"""
if ctx.invoked_subcommand is None:
ctx.invoke(repl)
#cli.command()
#click.option('--name', default='world')
def hello(name):
"""Say hello"""
click.echo('Hello, {}!'.format(name))
#cli.command()
#click.option('--name', default='moon')
def goodnight(name):
"""Say goodnight"""
click.echo('Goodnight, {}.'.format(name))
#cli.command()
def repl():
"""Start an interactive session"""
prompt_kwargs = {
'history': FileHistory(os.path.expanduser('~/.repl_history'))
}
click_repl.repl(click.get_current_context(), prompt_kwargs=prompt_kwargs)
if __name__ == '__main__':
cli(obj={})
Here's what it looks like to use the REPL:
$ python pleasantries.py
> hello
Hello, world!
> goodnight --name fpbhb
Goodnight, fpbhb.
And to use the command line subcommands directly:
$ python pleasntries.py goodnight
Goodnight, moon.
I know this is super old, but I've been working on fpbhb's solution to support options as well. I'm sure this could use some more work, but here is a basic example of how it could be done:
import click
import cmd
import sys
from click import BaseCommand, UsageError
class REPL(cmd.Cmd):
def __init__(self, ctx):
cmd.Cmd.__init__(self)
self.ctx = ctx
def default(self, line):
subcommand = line.split()[0]
args = line.split()[1:]
subcommand = cli.commands.get(subcommand)
if subcommand:
try:
subcommand.parse_args(self.ctx, args)
self.ctx.forward(subcommand)
except UsageError as e:
print(e.format_message())
else:
return cmd.Cmd.default(self, line)
#click.group(invoke_without_command=True)
#click.pass_context
def cli(ctx):
if ctx.invoked_subcommand is None:
repl = REPL(ctx)
repl.cmdloop()
#cli.command()
#click.option('--foo', required=True)
def a(foo):
print("a")
print(foo)
return 'banana'
#cli.command()
#click.option('--foo', required=True)
def b(foo):
print("b")
print(foo)
if __name__ == "__main__":
cli()
I was trying to do something similar to the OP, but with additional options / nested sub-sub-commands. The first answer using the builtin cmd module did not work in my case; maybe with some more fiddling.. But I did just run across click-shell. Haven't had a chance to test it extensively, but so far, it seems to work exactly as expected.