I have a case where I'd like to run a common function (check_upgrade()) for most of my click commands, but there are a few cases where I don't want to run it. Rather than relying on developers to remember to add a decorator or function call to explicitly run this check, I'd prefer to instead run it by default and then have a decorator that one can add (e.g. #bypass_upgrade_check) for commands where check_upgrade() should not run.
I was hoping for something like:
class State(object):
def __init__(self):
self.bypass_upgrade_check = False
pass_state = click.make_pass_decorator(State, ensure=True)
def bypass_upgrade_check(func):
#pass_state
def wrapper(state, *args, **kwargs):
state.bypass_upgrade_check = True
func(*args, **kwargs)
return wrapper
#click.group()
#pass_state
def common(state):
if not state.bypass_upgrade_check:
check_upgrade()
#common.command()
def cmd1():
# check_upgrade() runs here
pass
#bypass_upgrade_check
#common.command()
def cmd2():
# don't run check_upgrade() here
pass
But this doesn't work. It doesn't actually ever call the bypass_upgrade_check() function.
Is there a way to decorate a command in such a way that I can modify the state before the group code runs? Or another method altogether that accomplishes this?
To keep track of which commands bypass the upgrade check, I suggest that in the bypass marking decorator you store that state on the click.Command object. Then if you pass the click.Context to your group, you can then look at the command object to see if it is marked to allow skipping upgrade like:
Code:
def bypass_upgrade_check(func):
setattr(func, 'do_upgrade_check', False)
#click.group()
#click.pass_context
def cli(ctx):
sub_cmd = ctx.command.commands[ctx.invoked_subcommand]
if getattr(sub_cmd, 'do_upgrade_check', True):
check_upgrade()
Test Code:
import click
def check_upgrade():
click.echo('Checking Upgrade!')
def bypass_upgrade_check(func):
setattr(func, 'do_upgrade_check', False)
#click.group()
#click.pass_context
def cli(ctx):
sub_cmd = ctx.command.commands[ctx.invoked_subcommand]
if getattr(sub_cmd, 'do_upgrade_check', True):
check_upgrade()
#cli.command()
def cmd1():
# check_upgrade() runs here
click.echo('cmd1')
#bypass_upgrade_check
#cli.command()
def cmd2():
# don't run check_upgrade() here
click.echo('cmd2')
if __name__ == "__main__":
commands = (
'cmd1',
'cmd2',
)
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)
cli(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)]
-----------
> cmd1
Checking Upgrade!
cmd1
-----------
> cmd2
cmd2
Related
I'm trying to pass contexts between two subcommands with python-click. Here's an MWE:
import click
#click.group(chain=True)
def cli() -> None:
pass
#cli.command()
#click.pass_context
def fn1(cxt):
cxt.obj = 1
#cli.command()
#click.pass_context
def fn2(cxt):
print(f'{cxt.obj=}')
if __name__ == '__main__':
cli()
If I call this with cli.py fn1 fn2, I expect to get "cxt.obj=1", whereas I get "cxt.obj=None".
While trying to debug, I also noticed I can access the cli's context from fn1 and fn2, but not fn1's context from fn2. I can therefore, set the object of cli's context in fn1, and read from cli's context in fn2, but there must be a better way, where the obj is directly accessible in fn2's context.
What is wrong with my mental model, why doesn't the context persist between subcommands? And, what is the best pattern to use when one needs to pass data between subcommands?
The context is copied from a parent to a child. In your case context.obj for cli() which you do not expose in your example, is None. The None is copied into the child context for fn1() and fn2(), but then in fn1() you change the copy to 1. Finally, in fn2() you receive a copy of the context for cli() in which obj is still None
So to achieve your desired result there are (at least) two options.
Make context.obj on the parent context a mutable object, and then mutate said object
As you indicated was possible, directly access the parent context via something like ctx.parent.obj = 1
An example of the first (my preferred) method:
Example
import click, sys
#click.group(chain=True)
#click.pass_context
def cli(ctx) -> None:
ctx.obj = {}
#cli.command()
#click.pass_context
def fn1(ctx):
ctx.obj['fn1'] = 1
#cli.command()
#click.pass_context
def fn2(ctx):
click.echo(f'obj: {ctx.obj}')
Test Code
if __name__ == "__main__":
print('Click Version: {}'.format(click.__version__))
print('Python Version: {}'.format(sys.version))
print('-----------')
cmd = 'fn1 fn2'
print('> ' + cmd)
cli(cmd.split())
Results:
Click Version: 8.1.3
Python Version: 3.7.8 (tags/v3.7.8:4b47a5b6ba, Jun 28 2020, 08:53:46) [MSC v.1916 64 bit (AMD64)]
-----------
> fn1 fn2
obj: {'fn1': 1}
I want to use a value in my configuration (which I load into my context) as the default value for a click command option. I've read this section of the documentation, and I don't think I'm understanding what I need do.
This is my example script:
import sys
import click
#click.group()
#click.pass_context
def cli(ctx):
"""
CLI
"""
ctx.ensure_object(dict)
ctx.obj['DEFAULT_ENVIRONMENT'] = "dev"
#cli.command()
#click.option('-e', '--environment', required=True, default=click.get_current_context().obj['DEFAULT_ENVIRONMENT'])
def show_env(environment):
click.echo(environment)
if __name__ == '__main__':
cli()
The goal, if I run python cli.py show-env, is to get it to output dev (because I didn't pass the parameter as it's loaded from context).
This fails with
Traceback (most recent call last):
File "testcli.py", line 15, in <module>
#click.option('-e', '--environment', required=True, default=click.get_current_context().obj['DEFAULT_ENVIRONMENT'])
File "/home/devuser/.virtualenvs/cli/lib/python3.6/site-packages/click/globals.py", line 26, in get_current_context
raise RuntimeError('There is no active click context.')
RuntimeError: There is no active click context.
I have also tried by using #pass_context on my show_env command like so:
#cli.command()
#click.option('-e', '--environment', required=True, default=ctx.obj['DEFAULT_ENVIRONMENT'])
#click.pass_context
def show_env(ctx, environment):
click.echo(environment)
Which fails because ctx isn't defined at that point.
Traceback (most recent call last):
File "testcli.py", line 15, in <module>
#click.option('-e', '--environment', required=True, default=ctx.obj['DEFAULT_ENVIRONMENT'])
NameError: name 'ctx' is not defined
Am I able to use my context to set a command option default value?
As you noted, the context does not yet exist at the time you are trying to examine it. You can delay the default lookup from context until the context does exist, by using a custom class like:
Custom Class
def default_from_context(default_name):
class OptionDefaultFromContext(click.Option):
def get_default(self, ctx):
self.default = ctx.obj[default_name]
return super(OptionDefaultFromContext, self).get_default(ctx)
return OptionDefaultFromContext
Using the Custom Class
To use the custom class pass it to click.option via the cls parameter like:
#click.option('-e', '--environment', required=True,
cls=default_from_context('DEFAULT_ENVIRONMENT'))
How does this work?
This works because click is a well designed OO framework. The #click.option() decorator usually instantiates a click.Option object but allows this behavior to be over ridden with the cls parameter. So it is a relatively easy matter to inherit from click.Option in our own class and over ride desired methods.
In this case, we override click.Option.get_default(). In our get_default() we examine the context and set the default value. We then call the parent get_default() to continue further processing.
Test Code:
import click
#click.group()
#click.pass_context
def cli(ctx):
"""
CLI
"""
ctx.ensure_object(dict)
ctx.obj['DEFAULT_ENVIRONMENT'] = "dev"
#cli.command()
#click.option('-e', '--environment', required=True,
cls=default_from_context('DEFAULT_ENVIRONMENT'))
def show_env(environment):
click.echo(environment)
if __name__ == "__main__":
commands = (
'show_env',
'--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)
cli(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.5 (v3.6.5:f59c0932b4, Mar 28 2018, 05:52:31)
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.57)]
-----------
> show_env
dev
-----------
> --help
Usage: click_prog.py [OPTIONS] COMMAND [ARGS]...
CLI
Options:
--help Show this message and exit.
Commands:
show_env
To expand on Stephen Rauch's answer above, for Click 8, the signature of click.Option.get_default is get_default(self, ctx: Context, call: bool = True), so you might need to add the call argument.
In my python-click-CLI script, I'm building some commands for features that should not be visible for users (to not confuse them), but visible for e.g. developers.
Is it possible to use feature flags for Python-click commands?
I would like to be able to configure (via a config file, etc) if a command is available or not. If a command-feature is disabled, the command should not be callable and the help should not show it.
Like this:
FLAG_ENABLED = False
# This command should not be shown and not be callable as long as the flag is disabled
#cli.command(name='specialfeature', active=FLAG_ENABLED)
def special_feature_command()
....
Obviously, I could change the body of my function:
#cli.command(name='specialfeature', active=FLAG_ENABLED)
def special_feature_command()
if FLAG_ENABLED:
...
else:
...
But then my command would still show up in help, which I would like to avoid.
You can add a feature flag functionality with a custom class like:
Custom Class
This class over rides the click.Group.command() method which is used to decorate command functions. It adds the ability to pass an active flag, which when False will skip adding the command to the group.
import click
class FeatureFlagCommand(click.Group):
def command(self, *args, active=True, **kwargs):
"""Behaves the same as `click.Group.command()` except added an
`active` flag which can be used to disable to command.
"""
if active:
return super(FeatureFlagCommand, self).command(*args, **kwargs)
else:
return lambda f: f
Using the Custom Class
By passing the cls parameter to the click.group() decorator, any commands added to the group via the the group.command() will be gated with the active flag.
#click.group(cls=FeatureFlagCommand)
def cli():
...
#cli.command(name='specialfeature', active=FLAG_ENABLED)
def special_feature_command()
...
How does this work?
This works because click is a well designed OO framework. It is easy to inherit from click.Group and build a new command() decorator. In the new command() decorator if the active flag is False we return the undecorated function instead of adding the function to the group.
Test Code:
#click.group(cls=FeatureFlagCommand)
def cli():
"""My Awesome Click Program"""
#cli.command(active=False)
def command1():
click.echo('Command 1')
#cli.command(active=True)
def command2():
click.echo('Command 2')
#cli.command()
def command3():
click.echo('Command 3')
if __name__ == "__main__":
commands = (
'command1',
'command2',
'command3',
'--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)
cli(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)]
-----------
> command1
Usage: test.py [OPTIONS] COMMAND [ARGS]...
Error: No such command "command1".
-----------
> command2
Command 2
-----------
> command3
Command 3
-----------
> --help
Usage: test.py [OPTIONS] COMMAND [ARGS]...
My Awesome Click Program
Options:
--help Show this message and exit.
Commands:
command2
command3
-----------
>
Usage: test.py [OPTIONS] COMMAND [ARGS]...
My Awesome Click Program
Options:
--help Show this message and exit.
Commands:
command2
command3
I'm trying to make a verbose flag for my Python program.
Currently, I'm doing this:
import click
#global variable
verboseFlag = False
#parse arguments
#click.command()
#click.option('--verbose', '-v', is_flag=True, help="Print more output.")
def log(verbose):
global verboseFlag
verboseFlag = True
def main():
log()
if verboseFlag:
print("Verbose on!")
if __name__ == "__main__":
main()
It'll never print "Verbose on!" even when I set the '-v' argument. My thoughts are that the log function needs a parameter, but what do I give it? Also, is there a way to check whether the verbose flag is on without global variables?
So click is not simply a command line parser. It also dispatches and processes the commands. So in your example, the log() function never returns to main(). The intention of the framework is that the decorated function, ie: log(), will do the needed work.
Code:
import click
#click.command()
#click.option('--verbose', '-v', is_flag=True, help="Print more output.")
def log(verbose):
click.echo("Verbose {}!".format('on' if verbose else 'off'))
def main(*args):
log(*args)
Test Code:
if __name__ == "__main__":
commands = (
'--verbose',
'-v',
'',
'--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)]
-----------
> --verbose
Verbose on!
-----------
> -v
Verbose on!
-----------
>
Verbose off!
-----------
> --help
Usage: test.py [OPTIONS]
Options:
-v, --verbose Print more output.
--help Show this message and exit.
The above answer was helpful, but this is what I ended up using. I thought I'd share since so many people are looking at this question:
#click.command()
#click.option('--verbose', '-v', is_flag=True, help="Print more output.")
def main(verbose):
if verbose:
# do something
if __name__ == "__main__":
# pylint: disable=no-value-for-parameter
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.