I am trying to use the Python library Click, but struggle to get an example working. I defined two groups, one of which (group2) is meant to handle common parameters for this group of commands. What I want to achieve is that those common parameters get processed by the group function (group2) and assigned to the context variable, so they can be used by the actual commands.
A use case would be a number of commands that require username and password, while some others don't (not even optionally).
This is the code
import click
#click.group()
#click.pass_context
def group1(ctx):
pass
#click.group()
#click.option('--optparam', default=None, type=str)
#click.option('--optparam2', default=None, type=str)
#click.pass_context
def group2(ctx, optparam):
print 'in group2', optparam
ctx['foo'] = create_foo_by_processing_params(optparam, optparam2)
#group2.command()
#click.pass_context
def command2a(ctx):
print 'command2a', ctx['foo']
#group2.command()
#click.option('--another-param', default=None, type=str)
#click.pass_context
def command2b(ctx, another_param):
print 'command2b', ctx['foo'], another_param
# many more more commands here...
# #group2.command()
# def command2x():
# ...
#group1.command()
#click.argument('argument1')
#click.option('--option1')
def command1(argument1, option1):
print 'In command2', argument1, option1
cli = click.CommandCollection(sources=[group1, group2])
if __name__ == '__main__':
cli(obj={})
And this is the result when using command2:
$ python cli-test.py command2 --optparam=123
> Error: no such option: --optparam`
What's wrong with this example. I tried to follow the docs closely, but opt-param doesn't seem to be recognised.
The basic issue with the desired scheme is that click.CommandCollection does not call the group function. It skips directly to the command. In addition it is desired to apply options to the group via decorator, but have the options parsed by the command. That is:
> my_prog my_command --group-option
instead of:
> my_prog --group-option my_command
How?
This click.Group derived class hooks the command invocation for the commands to intercept the group parameters, and pass them to the group command.
In Group.add_command, add the params to the command
In Group.add_command, override command.invoke
In overridden command.invoke, take the special args inserted from the group and put them into ctx.obj and remove them from params
In overridden command.invoke, invoke the group command, and then the command itself
Code:
import click
class GroupWithCommandOptions(click.Group):
""" Allow application of options to group with multi command """
def add_command(self, cmd, name=None):
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 commands 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 the original invoke (the command)
original_invoke(ctx)
return command_invoke
Test Code:
#click.group()
#click.pass_context
def group1(ctx):
pass
#group1.command()
#click.argument('argument1')
#click.option('--option1')
def command1(argument1, option1):
click.echo('In command2 %s %s' % (argument1, option1))
#click.group(cls=GroupWithCommandOptions)
#click.option('--optparam', default=None, type=str)
#click.option('--optparam2', default=None, type=str)
#click.pass_context
def group2(ctx, optparam, optparam2):
# create_foo_by_processing_params(optparam, optparam2)
ctx.obj['foo'] = 'from group2 %s %s' % (optparam, optparam2)
#group2.command()
#click.pass_context
def command2a(ctx):
click.echo('command2a foo:%s' % ctx.obj['foo'])
#group2.command()
#click.option('--another-param', default=None, type=str)
#click.pass_context
def command2b(ctx, another_param):
click.echo('command2b %s %s' % (ctx['foo'], another_param))
cli = click.CommandCollection(sources=[group1, group2])
if __name__ == '__main__':
cli('command2a --optparam OP'.split())
Results:
command2a foo:from group2 OP None
This isn't the answer I am looking for, but a step towards it. Essentially a new kind of group is introduced (GroupExt) and the option added to the group is now being added to the command.
$ python cli-test.py command2 --optparam=12
cli
command2 12
import click
class GroupExt(click.Group):
def add_command(self, cmd, name=None):
click.Group.add_command(self, cmd, name=name)
for param in self.params:
cmd.params.append(param)
#click.group()
def group1():
pass
#group1.command()
#click.argument('argument1')
#click.option('--option1')
def command1(argument1, option1):
print 'In command2', argument1, option1
# Equivalent to #click.group() with special group
#click.command(cls=GroupExt)
#click.option('--optparam', default=None, type=str)
def group2():
print 'in group2'
#group2.command()
def command2(optparam):
print 'command2', optparam
#click.command(cls=click.CommandCollection, sources=[group1, group2])
def cli():
print 'cli'
if __name__ == '__main__':
cli(obj={})
This is not quite what I am looking for. Ideally, the optparam would be handled by group2 and the results placed into the context, but currently it's processed in the command2. Perhaps someone knows how to extend this.
Related
Let's take the example below. The parser contains two arguments --inputfile and verbosity. The Set_verbosity_level() function is used to controls the value of a module-level/global variable (in my real life a package-level variable) to 0-4. The CheckFile() function performs tests inside input file (in the real life depending on type).
I would like to print messages in CheckFile() depending on verbosity. The problem is that argparse calls CheckFile() before Set_verbosity_level() so the verbosity level is always 0/default in CheckFile...
So my question is whether there is any solution to force argparse to evaluate some arguments before others...
import argparse
VERBOSITY = 0
def Set_verbosity_level():
"""Set the verbosity level.
"""
def type_func(value):
a_value = int(value)
globals()['VERBOSITY'] = value
print("Verbosity inside Set_verbosity_level(): " + str(globals()['VERBOSITY']))
return value
return type_func
class CheckFile(argparse.FileType):
"""
Check whatever in the file
"""
def __init__(self, mode='r', **kwargs):
super(CheckFile, self).__init__(mode, **kwargs)
def __call__(self, string):
# Do whatever processing/checking/transformation
# e.g print some message according to verbosity
print("Verbosity inside CheckFile(): " + str(globals()['VERBOSITY']))
return super(CheckFile, self).__call__(string)
def make_parser():
"""The main argument parser."""
parser = argparse.ArgumentParser(add_help=True)
parser.add_argument("-V",
"--verbosity",
default=0,
type=Set_verbosity_level(),
help="Increase output verbosity.",
required=False)
parser.add_argument('-i', '--inputfile',
help="Input file",
type=CheckFile(mode='r'),
required=True)
return parser
if __name__ == '__main__':
myparser = make_parser()
args = myparser.parse_args()
print("Verbosity in Main: " + str(VERBOSITY))
Calling this script gives:
$python test.py -i test.bed -V 2
Verbosity inside CheckFile(): 0
Verbosity inside Set_verbosity_level(): 2
Verbosity in Main: 2
argparse processes the command-line arguments in the order that they are listed, so if you simply swap the order of the given options, it would output in the verbosity you want:
python test.py -V 2 -i test.bed
This outputs:
Verbosity inside Set_verbosity_level(): 2
Verbosity inside CheckFile(): 2
There's no way otherwise to tell argparse to process the command-line arguments in a different order than how they're listed.
I don't know that you can force an argparse variable to be read first, but you can use pythons built in command line parser in your main function:
import sys
# Your classes here #
if __name__ == '__main__':
verbosity = 0
for i, sysarg in enumerate(sys.argv):
if str(sysarg).strip().lower().replace('-','') in ['v', 'verbose']:
try:
verbosity = sys.argv[i + 1]
except IndexError:
print("No verbosity level specified")
# more code
Its not very elegant and it's not argparse, but it's one way to ensure you get the verbosity first.
You could also update your CheckFile class to include a verbosity checking function:
class CheckFile(argparse.FileType):
"""
Check whatever in the file
"""
def __init__(self, mode='r', **kwargs):
super(CheckFile, self).__init__(mode, **kwargs)
def _check_verbosity(self):
verbosity = 0
for i, sysarg in enumerate(sys.argv):
if str(sysarg).strip().lower().replace('-','') in ['v', 'verbose']:
try:
verbosity = sys.argv[i + 1]
except IndexError:
print("No verbosity level specified")
return verbosity
def __call__(self, string):
# Do whatever processing/checking/transformation
# e.g print some message according to verbosity
print("Verbosity inside CheckFile(): {}".format(self._check_verbosity()))
return super(CheckFile, self).__call__(string)
Again, I know it's not really an answer to your argparse question, but it is a solution for your problem
main
|--> src
|--> custom_calculation.py
|--> test_custom_calculation.py
custom_calculation.py
def calc_total(a,b):
return a+b
def calc_multiply(a,b):
return a*b
test_custom_calculation.py
import custom_calculation
def test_calc_sum():
total = custom_calculation.calc_total(10,10)
assert total == 20
def test_calc_multiply():
result = custom_calculation.calc_multiply(10,10)
assert result == 100
This is how i execute for simple modules.
cd main/src
python -m pytest
py.test -v
Learning python object oriented. Please help me if my code is wrong (could be even in importing module as well). Actual question here is, can i execute python (containing class) modules along with pytest and option parser ?
main
|--> A
|--> custom_calculation.py
|--> src
|--> test_custom_calculation.py
test_custom_calculation.py
from optparse import OptionParser
from A import *
import sys
class Test_Custom_Calculation():
def test_calc_sum():
total = custom_calculation.calc_total(10,10)
assert total == 20
def test_calc_multiply():
result = custom_calculation.calc_multiply(10,10)
assert result == 100
if __name__ == "__main__":
O = Test_Custom_Calculation()
parser = OptionParser()
parser.add_option("-a", "--a", dest="a", default=None,
help="Enter value of a")
parser.add_option("-b", "--b", dest="b", default=None,
help="Enter value of b")
parser.add_option("-o", "--o", dest="o", default=None,
help="specify operation to be performed")
(options, args) = parser.parse_args()
if options.a is None or options.b is None or options.c is None:
sys.exit("provide values of a,b and specify operation")
if options.c == "add":
O.test_calc_sum(a,b)
elif options.c == "mul":
O.test_calc_multiply(a,b)
else:
sys.exit("Specify appropriate operation")
without pytest, i can run this as python test_custom_calculation.py --a 10 --b 10 --c add
how can i run this with pytest ?
EDITED :
test_sample.py
def test_fun1(val1, val2, val3):
def test_fun2(val4,val5,val1):
def test_fun3(val6,val7,val8):
def test_fun4(val9,val10,val2):
def test_fun5(val2,val11,val10):
conftest.py
import pytest
def pytest_addoption(parser):
parser.add_option("-a", "--add", dest="add", default=None,
help="specifies the addition operation")
parser.add_option("-s", "--sub", dest="sub", default=None,
help="specifies the subtraction")
parser.add_option("-m", "--mul", dest="mul", default=None,
help="specifies the multiplication")
parser.add_option("-d", "--div", dest="div", default=None,
help="specifies the division")
parser.add_option("-t", "--trigonometry", dest="trigonometry", default=None,
help="specifies the trigonometry operation")
where to define those functional arguments val* ?
where can we decide the logic of handling optional parser ?
say, if option.add and option.sub:
sys.exit("Please provide only one option")
if option.add is None :
sys.exit("No value provided")
if option.add == "add":
test_fun1(val1,val2,val3)
According to your question, i understood that you want to pass operations(add,sub) as a command line parameters, and execute the operations with the various val*.
So in Pytest,
You can refer my answer:-- A way to add test specific params to each test using pytest
So it is based on test method name, logic should be handled in the fixture.
Yes, Pytest has inbuilt parser option for the testcase.
Defined the below method in conftest.py.
def pytest_addoption(parser):
"""
Command line options for the pytest tests in this module.
:param parser: Parser used for method.
:return: None
"""
parser.addoption("--o",
default=None,
actions="store"
help="specify operation to be performed")
Kindly refer https://docs.pytest.org/en/latest/example/simple.html for more detail.
Use command:--
pytest -vsx test_custom_calculations.py --a= --o=
In test method,
test_custom_calculations.py
def test_cal(request):
value_retrieved = request.config.getoption("--a")
Lets say I have the following test class in a tests.py:
class MyTest(unittest.TestCase):
#classmethod
def setUpClass(cls, ip="11.111.111.111",
browserType="Chrome",
port="4444",
h5_client_url="https://somelink.com/",
h5_username="username",
h5_password="pass"):
cls.driver = get_remote_webdriver(ip, port, browserType)
cls.driver.implicitly_wait(30)
cls.h5_client_url = h5_client_url
cls.h5_username = h5_username
cls.h5_password = h5_password
#classmethod
def tearDownClass(cls):
cls.driver.quit()
def test_01(self):
# test code
def test_02(self):
# test code
...
def test_N(self):
# test code
All my tests (test_01 to test_N) use the parameters, provided in the setUpClass. Those parameters have default values:
ip="11.111.111.111",
browserType="Chrome",
port="4444",
h5_client_url="https://somelink.com/",
h5_username="username",
h5_password="pass"
So I wonder if I can inject new values for those parameters. And I want to do it from another python script so there will be no changes or just minor changes to code of the tests.
Note: I want to run my tests by a batch/shell command and save the output of the test to a log file (to redirect the standard output to that log file)
One think I did was to create a function decorator, that passes a dictionary with key=parameter_name and value=parameter_new_value, but I had to write to much additional code in the tests.py:
I defined the function_decorator logic
I put that #function_decorator annotation above every function I want to decorate
That function decorator needs that dictionary as a parameter, so I made a main, that looks something like that:
if __name__ == '__main__':
# terminal command to run tests should look like this /it is executed by the run-test PARROT command/
# python [this_module_name] [dictionary_containing_parameters] [log_file.log] *[tests]
parser = argparse.ArgumentParser()
# add testbeds_folder as scripts' first parameter, test_log_file as second and tests as the rest
parser.add_argument('dictionary_containing_parameters')
parser.add_argument('test_log_file')
parser.add_argument('unittest_args', nargs='*')
args = parser.parse_args()
dictionary_containing_parameters = sys.argv[1]
test_log_file = sys.argv[2]
# removes the "dictionary_containing_parameters" and "test_log_file" from sys.args - otherwise an error occurs unittest TestRunner
sys.argv[1:] = args.unittest_args
# executes the test/tests and save the output to the test_log_file
with open(test_log_file, "w") as f:
runner = unittest.TextTestRunner(f)
unittest.main(defaultTest=sys.argv[1:], exit=False, testRunner=runner)
Here is one possible solution:
You run your test from a different module this way:
if __name__ == '__main__':
testbed_dict = {"ip": "11.111.111.112",
"browserType": "Chrome",
"port": "4444",
"h5_client_url": "https://new_somelink.com/",
"h5_username": "new_username",
"h5_password": "new_pass"}
sys.argv.append(testbed_dict)
from your_tests_module import *
with open("test.log", "w") as f:
runner = unittest.TextTestRunner(f)
unittest.main(argv=[sys.argv[0]], defaultTest='test_class.test_name', exit=False, testRunner=runner)
You can nottice that argv=[sys.argv[0]] in unittest.main(argv=[sys.argv[0]], defaultTest='test_class.test_name', exit=False, testRunner=runner). Doing that you change the unittests argument to one (no error occurs) to a list with your real arguments. Note that at the end of this list is the dictionary with the new values of test parameters.
Ok, now you write an function decorator, that should look like this:
def load_params(system_arguments_list):
def decorator(func_to_decorate):
#wraps(func_to_decorate)
def wrapper(self, *args, **kwargs):
kwargs = system_arguments_list[-1]
return func_to_decorate(self, **kwargs)
return wrapper
return decorator
And use this decorator this way:
#classmethod
#load_params(sys.argv)
def setUpClass(cls, ip="11.111.111.111",
browserType="Chrome",
port="4444",
h5_client_url="https://somelink.com/",
h5_username="username",
h5_password="pass"):
cls.driver = get_remote_webdriver(ip, port, browserType)
cls.driver.implicitly_wait(30)
cls.h5_client_url = h5_client_url
cls.h5_username = h5_username
Basically imagine that I have argparser that has multiple arguments.
I have a particular function definition that looks like this:
def add_to_parser(self, parser):
group = parser.add_argument_group('')
group.add_argument( '--deprecateThis', action='throw exception', help='Stop using this. this is deprecated')
Whether I can try and create that action to throw an exception and stop the code or if I can wrap it to check for the deprecateThis flag and then throw an exception, I'd like to know how to do it and which is best! Thanks.
Here's what I came up with:
You can register custom actions for your arguments, I registered one to print out a deprecation warning and remove the item from the resulting namespace:
class DeprecateAction(argparse.Action):
def __init__(self, *args, **kwargs):
self.call_count = 0
if 'help' in kwargs:
kwargs['help'] = f'[DEPRECATED] {kwargs["help"]}'
super().__init__(*args, **kwargs)
def __call__(self, parser, namespace, values, option_string=None):
if self.call_count == 0:
sys.stderr.write(f"The option `{option_string}` is deprecated. It will be ignored.\n")
sys.stderr.write(self.help + '\n')
delattr(namespace, self.dest)
self.call_count += 1
if __name__ == "__main__":
my_parser = ArgumentParser('this is the description')
my_parser.register('action', 'ignore', DeprecateAction)
my_parser.add_argument(
'-f', '--foo',
help="This argument is deprecated",
action='ignore')
args = my_parser.parse_args()
# print(args.foo) # <- would throw an exception
Say my CLI utility has three commands: cmd1, cmd2, cmd3
And I want cmd3 to have same options and flags as cmd1 and cmd2. Like some sort of inheritance.
#click.command()
#click.options("--verbose")
def cmd1():
pass
#click.command()
#click.options("--directory")
def cmd2():
pass
#click.command()
#click.inherit(cmd1, cmd2) # HYPOTHETICAL
def cmd3():
pass
So cmd3 will have flag --verbose and option --directory. Is it possible to make this with Click? Maybe I just have overlooked something in the documentation...
EDIT: I know that I can do this with click.group(). But then all the group's options must be specified before group's command. I want to have all the options normally after command.
cli.py --verbose --directory /tmp cmd3 -> cli.py cmd3 --verbose --directory /tmp
I have found a simple solution! I slightly edited the snippet from https://github.com/pallets/click/issues/108 :
import click
_cmd1_options = [
click.option('--cmd1-opt')
]
_cmd2_options = [
click.option('--cmd2-opt')
]
def add_options(options):
def _add_options(func):
for option in reversed(options):
func = option(func)
return func
return _add_options
#click.group()
def group(**kwargs):
pass
#group.command()
#add_options(_cmd1_options)
def cmd1(**kwargs):
print(kwargs)
#group.command()
#add_options(_cmd2_options)
def cmd2(**kwargs):
print(kwargs)
#group.command()
#add_options(_cmd1_options)
#add_options(_cmd2_options)
#click.option("--cmd3-opt")
def cmd3(**kwargs):
print(kwargs)
if __name__ == '__main__':
group()
Define a class with common parameters
class StdCommand(click.core.Command):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.params.insert(0, click.core.Option(('--default-option',), help='Every command should have one'))
Then pass the class to decorator when defining the command function
#click.command(cls=StdCommand)
#click.option('--other')
def main(default_option, other):
...
You could also have another decorator for shared options. I found this solution here
def common_params(func):
#click.option('--foo')
#click.option('--bar')
#functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
#click.command()
#common_params
#click.option('--baz')
def cli(foo, bar, baz):
print(foo, bar, baz)
This code extracts all the options from it's arguments
def extract_params(*args):
from click import Command
if len(args) == 0:
return ['']
if any([ not isinstance(a, Command) for a in args ]):
raise TypeError('Handles only Command instances')
params = [ p.opts() for cmd_inst in args for p in cmd_inst.params ]
return list(set(params))
now you can use it:
#click.command()
#click.option(extract_params(cmd1, cmd2))
def cmd3():
pass
This code extracts only the parameters and none of their default values, you can improve it if needed.
A slight improvement on #jirinovo solution.
this version support an unlimited number of click options.
one thing that is worth mentioning, the order you pass the options is important
import click
_global_options = [click.option('--foo', '-f')]
_local_options = [click.option('--bar', '-b', required=True)]
_local_options2 = [click.option('--foofoo', required=True)]
def add_options(*args):
def _add_options(func):
options = [x for n in args for x in n]
for option in reversed(options):
func = option(func)
return func
return _add_options
#click.group()
def cli():
pass
#cli.group()
def subcommand():
pass
#subcommand.command()
#add_options(_global_options, _local_options)
def echo(foo, bar):
print(foo, bar, sep='\n')
#subcommand.command()
#add_options(_global_options)
def echo2(foo):
print(foo)
#subcommand.command()
#add_options(_global_options, _local_options2)
def echo3(foo, foofoo):
print(foo, foofoo, sep='\n')
#subcommand.command()
#add_options(_global_options, _local_options, _local_options2)
def echo4(foo, bar, foofoo):
print(foo, bar, foofoo, sep='\n')
if __name__ == '__main__':
cli()