Click password option only if argument equals something - python

In click, I'm defining this command:
#click.command('time', short_help='Timesheet Generator')
#click.argument('time_command', type=click.Choice(['this', 'last']))
#click.argument('data_mode', type=click.Choice(['excel', 'exchange']), default='exchange')
#click.option('--password', prompt=True, hide_input=True, confirmation_prompt=False)
#pass_context
def cli(ctx, time_command, data_mode, password):
The issue I have is that I only want the password to prompt if the data_mode argument equals exchange. How can I pull this off?

We can remove the need for a prompt if another parameter does not match a specific value by building a custom class derived from click.Option, and in that class over riding the click.Option.handle_parse_result() method like:
Custom Class:
import click
def PromptIf(arg_name, arg_value):
class Cls(click.Option):
def __init__(self, *args, **kwargs):
kwargs['prompt'] = kwargs.get('prompt', True)
super(Cls, self).__init__(*args, **kwargs)
def handle_parse_result(self, ctx, opts, args):
assert any(c.name == arg_name for c in ctx.command.params), \
"Param '{}' not found for option '{}'".format(
arg_name, self.name)
if arg_name not in opts:
raise click.UsageError(
"Illegal usage: `%s` is a required parameter with" % (
arg_name))
# remove prompt from
if opts[arg_name] != arg_value:
self.prompt = None
return super(Cls, self).handle_parse_result(ctx, opts, args)
return Cls
Using Custom Class:
To use the custom class, pass the cls parameter to click.option decorator like:
#click.option('--an_option', cls=PromptIf('an_argument', 'an_arg_value'))
pass in the name of the parameter to examine for the desired value, and the value to check for.
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 overridden with the cls parameter. So it is a relatively easy matter to inherit from click.Option in our own class and over ride the desired methods.
In this case we over ride click.Option.handle_parse_result() and disable the need to prompt if the other specified parameter does not match the desired.
Note: This answer was inspired by this answer.
Test Code:
#click.command()
#click.argument('an_argument', type=click.Choice(['excel', 'exchange']),
default='exchange')
#click.option('--password', hide_input=True, confirmation_prompt=False,
cls=PromptIf('an_argument', 'exchange'))
def cli(an_argument, password):
click.echo(an_argument)
click.echo(password)
cli('exchange'.split())

You can try splitting this up to multiple commands.
For example, time would be the entry point command. Either time_excel or time_exchange would then be invoked by time based on the value of data_mode. One could have a password prompt while the other wouldn't.
See Invoking Other Commands in Click's documentation.

Related

Need Design Pattern Suggestion

I need help para to beautify this code :)
The method definesAction will call a Class, based on the args. There is some way to generalizing this piece of code, taking into account that the Class's are similar.
Thanks in advance
Main Class
def defineAction(args):
if args.classabc is not None:
for host in config.getList('ABC', 'hosts'):
class_abc = ClassABC(config.getConfigs('ABC', host), args.version[0], user, password)
class_abc.action(args.classabc)
if args.classxyz is not None:
for host in config.getList('XYZ', 'hosts'):
class_xyz = ClassXYZ(config.getConfigs('XYZ', host), args.version[0], user, password)
class_xyz.action(args.classxyz)
# ...
def main():
parser.add_argument('--classabc', choices=['cmd'])
parser.add_argument('--classxyz', choices=['cmd'])
# ...
args = parser.parse_args()
defineAction(args)
SubClasses
class ClassABC:
def __init__(self, configs, user, password):
self.hostConfigs = configs['host']
self.host_username = user
self.host_password = password
def a_method(self):
# This Method is equal in all subclasses
def b_method(self):
# This Method is different all subclasses
def action(self, action):
self.a_method()
self.b_method()
if action == 'cmd':
self.execute_cmd()
Config FILE
[ABC]
hosts=abc_host1
var_abc=value1
[XYZ]
hosts=xyz_host1,xyz_host2
var_xyz=value2
I'm working the assumption that the switches are mutually exclusive (in which case you really want to use a mutually exclusive argument group).
You want the argparser action to set the class. If your command-line switch doesn't need to take any arguments, then I'd use action="store_const" here:
parser.add_argument(
'--classabc', dest="class_", const=ClassABC,
action="store_const")
parser.add_argument(
'--classxyz', dest="class_", const=ClassXYZ,
action="store_const")
On parsing, the above actions set args.class_ to ClassABC or ClassXYZ when one or the other switch is used. Give the classes a class method or an attribute to determine what configuration section to look in, do not hardcode those names anywhere else.
For instance, if both classes have a config_section attribute, (set to 'ABC' for ClassABC and 'XYZ' for ClassXZY), then you can use that attribute in your loop creating instances:
if args.class_:
for host in config.getList(class_.config_section, 'hosts'):
instance = args.class_(config.getConfig(class_.config_section, host), ...)
The idea is to not switch based on args attributes, you can leave this to argparse as it is already determining the different options for you.
If both command-line switches require an additional argument, then create a custom Action subclass:
class StoreClassAction(argparse.Action):
def __call__(self, parser, namespace, values, **kwargs):
setattr(namespace, self.dest, (self.const, values)
then use this as:
parser.add_argument(
'--classabc', dest="class_", choices=['cmd'], const=ClassABC,
action=StoreClassAction)
parser.add_argument(
'--classxyz', dest="class_", choices=['cmd'], const=ClassXYZ,
action=StoreClassAction)
Now the args.class_ argument is set to (classobject, argumentvalue), so you can use:
if args.class_:
cls, action = args.class_
for host in config.getList(cls.config_section, 'hosts'):
instance = args.class_(config.getConfig(cls.config_section, host), ...)
instance.action(action)

Python Click multiple command names

Is it possible to do something like this with Python Click?
#click.command(name=['my-command', 'my-cmd'])
def my_command():
pass
I want my command lines to be something like:
mycli my-command
and
mycli my-cmd
but reference the same function.
Do I need to do a class like AliasedGroup?
AliasedGroup is not what you are after, since it allows a shortest prefix match, and it appears you need actual aliases. But that example does provide hints in a direction that can work. It inherits from click.Group and overides some behavior.
Here is a one way to approach what you are after:
Custom Class
This class overides the click.Group.command() method which is used to decorate command functions. It adds the ability to pass a list of command aliases. This class also adds a short help which references the aliased command.
class CustomMultiCommand(click.Group):
def command(self, *args, **kwargs):
"""Behaves the same as `click.Group.command()` except if passed
a list of names, all after the first will be aliases for the first.
"""
def decorator(f):
if isinstance(args[0], list):
_args = [args[0][0]] + list(args[1:])
for alias in args[0][1:]:
cmd = super(CustomMultiCommand, self).command(
alias, *args[1:], **kwargs)(f)
cmd.short_help = "Alias for '{}'".format(_args[0])
else:
_args = args
cmd = super(CustomMultiCommand, self).command(
*_args, **kwargs)(f)
return cmd
return decorator
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() can be passed a list of command names.
#click.group(cls=CustomMultiCommand)
def cli():
"""My Excellent CLI"""
#cli.command(['my-command', 'my-cmd'])
def my_command():
....
Test Code:
import click
#click.group(cls=CustomMultiCommand)
def cli():
"""My Excellent CLI"""
#cli.command(['my-command', 'my-cmd'])
def my_command():
"""This is my command"""
print('Running the command')
if __name__ == '__main__':
cli('--help'.split())
Test Results:
Usage: my_cli [OPTIONS] COMMAND [ARGS]...
My Excellent CLI
Options:
--help Show this message and exit.
Commands:
my-cmd Alias for 'my-command'
my-command This is my command
Here is a simpler way to solve the same thing:
class AliasedGroup(click.Group):
def get_command(self, ctx, cmd_name):
try:
cmd_name = ALIASES[cmd_name].name
except KeyError:
pass
return super().get_command(ctx, cmd_name)
#click.command(cls=AliasedGroup)
def cli():
...
#click.command()
def install():
...
#click.command()
def remove():
....
cli.add_command(install)
cli.add_command(remove)
ALIASES = {
"it": install,
"rm": remove,
}
Since this question has been asked, a click-aliases library has been created.
It works a bit like the other answers except that you don’t have to declare the command class by yourself:
import click
from click_aliases import ClickAliasedGroup
#click.group(cls=ClickAliasedGroup)
def cli():
pass
#cli.command(aliases=['my-cmd'])
def my_command():
pass

Optional argument in command with click

I am trying to accomplish something not very standard for CLI parsing with Click and it only works partially:
main CLI has multiple sub-commands (in sample below 'show' and 'check')
both those commands might have optional argument, but the argument is preceding them not following
I decided to handle that argument in the group "above" it and pass the value in the context
Sample:
import click
#click.group()
#click.argument('hostname', required=False)
#click.pass_context
def cli(ctx, hostname=None):
""""""
ctx.obj = hostname
click.echo("cli: hostname={}".format(hostname))
#cli.command()
#click.pass_obj
def check(hostname):
click.echo("check: hostname={}".format(hostname))
#cli.command()
#click.pass_obj
def show(hostname):
click.echo("check: hostname={}".format(hostname))
if __name__ == '__main__':
cli()
The part WITH the hostname works:
> pipenv run python cli.py localhost check
cli: hostname=localhost
check: hostname=localhost
> pipenv run python cli.py localhost show
cli: hostname=localhost
check: hostname=localhost
But the part WITHOUT the hostname DOES NOT:
> pipenv run python cli.py show
Usage: cli.py [OPTIONS] [HOSTNAME] COMMAND [ARGS]...
Error: Missing command.
Anybody has an idea about the direction I should start looking into?
This can be done by over riding the click.Group argument parser like:
Custom Class:
class MyGroup(click.Group):
def parse_args(self, ctx, args):
if args[0] in self.commands:
if len(args) == 1 or args[1] not in self.commands:
args.insert(0, '')
super(MyGroup, self).parse_args(ctx, args)
Using Custom Class:
Then to use the custom group, pass it as the cls argument to the group decorator like:
#click.group(cls=MyGroup)
#click.argument('hostname', required=False)
#click.pass_context
def cli(ctx, hostname=None):
....
How?
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.parse_args() and if the first parameter matches a command and the second parameter does not, then we insert an empty string as the first parameter. This puts everything back where the parser expects it to be.

Python Click: make option depend on previous option

Is there an idiomatic way, using the Python Click library, to create a command where one option depends on a value set by a previous option?
A concrete example (my use case) would be that a command takes an option of type click.File as input, but also an encoding option which specifies the encoding of the input stream:
import click
#click.command()
#click.option("--encoding", type=str, default="utf-8")
#click.option("--input",
type=click.File("r", encoding="CAN I SET THIS DYNAMICALLY BASED ON --encoding?"))
def cli(encoding, input):
pass
I guess it would have to involve some kind of deferred evaluation using a callable, but I'm not sure if it's even possible given the current Click API.
I've figured out I can do something along the following lines:
import click
#click.command()
#click.pass_context
#click.option("--encoding", type=str, default="utf-8")
#click.option("--input", type=str, default="-")
def cli(ctx, encoding, input):
input = click.File("r", encoding=encoding)(input, ctx=ctx)
But it somehow feels less readable / maintainable to decouple the option decorator from the semantically correct type constraint that applies to it, and put str in there instead as a dummy. So if there's a way to keep these two together, please enlighten me.
A proposed workaround:
I guess I could use the click.File type twice, making it lazy in the decorator so that the file isn't actually left opened, the first time around:
#click.option("--input", type=click.File("r", lazy=True), default="-")
This feels semantically more satisfying, but also redundant.
It is possible to inherit from the click.File class and override the .convert() method to allow it to gather the encoding value from the context.
Using a Custom Class
It should look something like:
#click.command()
#click.option("--my_encoding", type=str, default="utf-8")
#click.option("--in_file", type=CustomFile("r", encoding_option_name="my_encoding"))
def cli(my_encoding, in_file):
....
CustomFile should allow the user to specify whichever name they want for the parameter from which the encoding value should be collected, but there can be a reasonable default such as "encoding".
Custom File Class
This CustomFile class can be used in association with an encoding option:
import click
class CustomFile(click.File):
"""
A custom `click.File` class which will set its encoding to
a parameter.
:param encoding_option_name: The 'name' of the encoding parameter
"""
def __init__(self, *args, encoding_option_name="encoding", **kwargs):
# enforce a lazy file, so that opening the file is deferred until after
# all of the command line parameters have been processed (--encoding
# might be specified after --in_file)
kwargs['lazy'] = True
# Python 3 can use just super()
super(CustomFile, self).__init__(*args, **kwargs)
self.lazy_file = None
self.encoding_option_name = encoding_option_name
def convert(self, value, param, ctx):
"""During convert, get the encoding from the context."""
if self.encoding_option_name not in ctx.params:
# if the encoding option has not been processed yet, wrap its
# convert hook so that it also retroactively modifies the encoding
# attribute on self and self.lazy_file
encoding_opt = [
c for c in ctx.command.params
if self.encoding_option_name == c.human_readable_name]
assert encoding_opt, \
"option '{}' not found for encoded_file".format(
self.encoding_option_name)
encoding_type = encoding_opt[0].type
encoding_convert = encoding_type.convert
def encoding_convert_hook(*convert_args):
encoding_type.convert = encoding_convert
self.encoding = encoding_type.convert(*convert_args)
self.lazy_file.encoding = self.encoding
return self.encoding
encoding_type.convert = encoding_convert_hook
else:
# if it has already been processed, just use the value
self.encoding = ctx.params[self.encoding_option_name]
# Python 3 can use just super()
self.lazy_file = super(CustomFile, self).convert(value, param, ctx)
return self.lazy_file

How do I get the user specified list of optional command line args not containing the defaults using argparse in python?

I want to know which options were explicitly passed through command-line.
Consider the following argparse setup in test.py:
parser.add_argument("--foo", default=True, action="store_true")
parser.add_argument("--bar", default=False, action="store_true")
When I execute ./test.py --foo --bar, I shall get foo=True, bar=True in the Namespace.
In this case, --foo and --bar were passed explicitly through command-line.
When I execute ./test.py --bar, I shall still get foo=True, bar=True in the Namespace.
So, I need to find which args were actually passed while executing through command-line (in the 2nd case : --bar), without sacrificing the defaults functionality.
One approach is to search in argv, but it's not efficient and doesn't look elegant.
I want to know, if there is any argparse api or any other better approach which shall allow me to do this?
Simply set no default. If the variable is not set, the user did not pass it. After checking that, you can handle the default yourself.
With a 'store_true' action, the builtin default is 'False'
With
parser.add_argument('--foo',action='store_true')
no input produces
Namespace(foo=False)
while '--foo' produces
Namespace(foo=True)
with
parser.add_argument('--foo',action='store_true', default=True)
it is always foo=True. That argument is useless. DO NOT set your own default when using 'store_true' or 'store_false'.
If you want to know whether the user gave you a --foo or not, use the first form, and check whether the namespace value is true or not. If in later code you need foo to be True regardless of what the user gave you, set it explicitly, after you have used argparse.
The answers and comments here recommended to not set defaults, and then handle the defaults on my own within the code. However, the add_argument calls aren't completely under my control, so this wasn't really an option.
Initially I went with checking the presence of the options in sys.argv. This approach quickly proved inefficient, bug-prone and not at all scalable.
Finally, I ended up with this which seems to be working just fine:
class _Reflection(object):
def __init__(self, source, reflection, name=None):
self.source = source
self.reflection = reflection
self.name = name
def __getattr__(self, attribute):
self.attribute = attribute
return _Reflection(self.source.__getattribute__(attribute), self.reflection.__getattribute__(attribute), name=attribute)
def __call__(self, *args, **kwargs):
source_output = self.source(*args, **kwargs)
if self.name == 'add_argument':
# if the method being called is 'add_argument',
# over-ride the 'default' argument's value to 'None' in our secondary argparser.
kwargs['default'] = None
reflection_output = self.reflection(*args, **kwargs)
return _Reflection(source_output, reflection_output)
class ReflectionArgumentParser(object):
def create(self, *args, **kwargs):
self.parser = argparse.ArgumentParser(*args, **kwargs)
self._mirror = argparse.ArgumentParser(*args, **kwargs)
return _Reflection(self.parser, self._mirror)
def parse_args(self, *args, **kwargs):
return self.parser.parse_args(*args, **kwargs)
def filter_defaults(self, *args, **kwargs):
return self._mirror.parse_args(*args, **kwargs)
mirrorParser = ReflectionArgumentParser()
parser = mirrorParser.create()
parser.add_argument('-f', '--foo', default=False, action="store_true")
parser.add_argument('-b', '--baz', default=0, action="store_const", const=10)
parser.add_argument('bar', nargs='*', default='bar')
print mirrorParser.parse_args([])
# Outputs: Namespace(bar='bar', baz=0, foo=False)
print mirrorParser.filter_defaults([])
# Outputs: Namespace(bar=[], baz=None, foo=None)
print mirrorParser.filter_defaults('--foo -b lorem ipsum'.split())
# Outputs: Namespace(bar=['lorem', 'ipsum'], baz=10, foo=True)
I have tried this implementation with argument-groups and subparsers.
This doesn't deal with set_defaults method, however the additions required are trivial.
This is possible using the ArgumentParser.get_default(dest) method.
Basically, you iterate over all of the parsed arguments and collect which ones are not equal to the default value:
args = parser.parse_args()
non_default = {arg: value for (arg, value) in vars(args).iteritems() if value != parser.get_default(arg)}
Although this doesn't work in your specific example because --foo is a do-nothing argument (it sets the variable to the default value).

Categories