Python Click - Supply arguments and options from a configuration file - python

Given the following program:
#!/usr/bin/env python
import click
#click.command()
#click.argument("arg")
#click.option("--opt")
#click.option("--config_file", type=click.Path())
def main(arg, opt, config_file):
print("arg: {}".format(arg))
print("opt: {}".format(opt))
print("config_file: {}".format(config_file))
return
if __name__ == "__main__":
main()
I can run it with the arguments and options provided through command line.
$ ./click_test.py my_arg --config_file my_config_file
arg: my_arg
opt: None
config_file: my_config_file
How do I provide a configuration file (in ini? yaml? py? json?) to --config_file and accept the content as the value for the arguments and options?
For instance, I want my_config_file to contain
opt: my_opt
and have the output of the program show:
$ ./click_test.py my_arg --config_file my_config_file
arg: my_arg
opt: my_opt
config_file: my_config_file
I've found the callback function which looked to be useful but I couldn't find a way to modify the sibling arguments/options to the same function.

This can be done by over riding the click.Command.invoke() method like:
Custom Class:
def CommandWithConfigFile(config_file_param_name):
class CustomCommandClass(click.Command):
def invoke(self, ctx):
config_file = ctx.params[config_file_param_name]
if config_file is not None:
with open(config_file) as f:
config_data = yaml.safe_load(f)
for param, value in ctx.params.items():
if value is None and param in config_data:
ctx.params[param] = config_data[param]
return super(CustomCommandClass, self).invoke(ctx)
return CustomCommandClass
Using Custom Class:
Then to use the custom class, pass it as the cls argument to the command decorator like:
#click.command(cls=CommandWithConfigFile('config_file'))
#click.argument("arg")
#click.option("--opt")
#click.option("--config_file", type=click.Path())
def main(arg, opt, config_file):
Test Code:
# !/usr/bin/env python
import click
import yaml
#click.command(cls=CommandWithConfigFile('config_file'))
#click.argument("arg")
#click.option("--opt")
#click.option("--config_file", type=click.Path())
def main(arg, opt, config_file):
print("arg: {}".format(arg))
print("opt: {}".format(opt))
print("config_file: {}".format(config_file))
main('my_arg --config_file config_file'.split())
Test Results:
arg: my_arg
opt: my_opt
config_file: config_file

I realize that this is way old, but since Click 2.0, there's a more simple solution. The following is a slight modification of the example from the docs.
This example takes explicit --port args, it'll take an environment variable, or a config file (with that precedence).
Command Groups
Our code:
import os
import click
from yaml import load
try:
from yaml import CLoader as Loader
except ImportError:
from yaml import Loader
#click.group(context_settings={'auto_envvar_prefix': 'FOOP'}) # this allows for environment variables
#click.option('--config', default='~/config.yml', type=click.Path()) # this allows us to change config path
#click.pass_context
def foop(ctx, config):
if os.path.exists(config):
with open(config, 'r') as f:
config = load(f.read(), Loader=Loader)
ctx.default_map = config
#foop.command()
#click.option('--port', default=8000)
def runserver(port):
click.echo(f"Serving on http://127.0.0.1:{port}/")
if __name__ == '__main__':
foop()
Assuming our config file (~/config.yml) looks like:
runserver:
port: 5000
and we have a second config file (at ~/config2.yml) that looks like:
runserver:
port: 9000
Then if we call it from bash:
$ foop runserver
# ==> Serving on http://127.0.0.1:5000/
$ FOOP_RUNSERVER_PORT=23 foop runserver
# ==> Serving on http://127.0.0.1:23/
$ FOOP_RUNSERVER_PORT=23 foop runserver --port 34
# ==> Serving on http://127.0.0.1:34/
$ foop --config ~/config2.yml runserver
# ==> Serving on http://127.0.0.1:9000/
Single Commands
If you don't want to use command groups and want to have configs for a single command:
import os
import click
from yaml import load
try:
from yaml import CLoader as Loader
except ImportError:
from yaml import Loader
def set_default(ctx, param, value):
if os.path.exists(value):
with open(value, 'r') as f:
config = load(f.read(), Loader=Loader)
ctx.default_map = config
return value
#click.command(context_settings={'auto_envvar_prefix': 'FOOP'})
#click.option('--config', default='config.yml', type=click.Path(),
callback=set_default, is_eager=True, expose_value=False)
#click.option('--port')
def foop(port):
click.echo(f"Serving on http://127.0.0.1:{port}/")
will give similar behavior.

Related

Python Typer and multiple inputs to CLI option

I'm trying to use Python Typer to create a CLI with an option input that:
takes multiple values
is of a List type.
For example:
$ ./my_cli --device 1 2
...
Got unexpected extra argument (2)
My code looks like this:
def main(
devices: List[str] = typer.Option([], help="Devices to query"),
):
print(f"CLI option is {devices}")
Any ideas?
from typing import List, Optional
import typer
def main(device: Optional[List[str]] = typer.Option(None)):
if not device:
print("No provided devices")
raise typer.Abort()
for d in device:
print(f"Processing device: {d}")
if __name__ == "__main__":
typer.run(main)
And then it can be used like in:
$ ./my_cli --device 1 --device 2
Take a look at the documentation.
If the only input for the CLI is a list of devices, you can simplify it to using arguments instead. Then it could be used like:
$ ./my_cli 1 2
from pathlib import Path
from typing import List
import typer
def main(files: List[Path], celebration: str):
for path in files:
if path.is_file():
print(f"This file exists: {path.name}")
print(celebration)
if __name__ == "__main__":
typer.run(main)

os.system argument from config file

I made this activity and it works. I need to have config file with USB/VID/PID.
def resetactivity():
os.system(r'"devcon.exe restart "*USB\VID_04E8&PID_3321*"')
I try to do this with config parser. I made config.txt:
[My Section]
usbdev = r'"devcon.exe restart "*USB\VID_04E8&PID_3321*"'
I read my config file in Python:
config = configparser.ConfigParser()
config.read('config.txt')
usbdev = config.get('My Section', 'usbdev')
And when I am trying to use this in os.system command like this:
def resetactivity():
os.system(usbdev)
I get this result:
The filename, directory name, or volume label syntax is incorrect.
'PID_3321*"'' is not recognized as an internal or external command,
operable program or batch file.
Try this code
import configparser
import os
def resetactivity():
config = configparser.ConfigParser()
config.read('config.txt')
usbdev = config.get('My Section', 'usbdev')
print(usbdev)
os.system(usbdev)
if __name__ == "__main__":
resetactivity()
With config.txt formatted as
[My Section]
usbdev = devcon.exe restart "USB\VID_04E8&PID_3321"

How to access config.option command line argument in test script with py.test framework

I am taking value of setup from command line argument while running the test with py.test framework.
group.addoption("--setup", "--sC=", action="store", dest="setup", help="setup.")
def pytest_configure(config):
print "config.option.setup: ", config.option.setup
Here, I am able to get the setup file name with config.option.setup, but the same file name which I pass here, I want to fetch it from my test script.
If I put the same line in my test script, I get below error:
> print "config.option.setup_config: ", config.option.setup_config
E NameError: global name 'config' is not defined
Can someone please let me know how can I access config.option.setup in my test script?
pytest_configure must be in the file conftest.py. See example:
option = None
def pytest_addoption(parser):
parser.addoption("--setup", "--sC=", action="store", dest="setup", help="setup.")
def pytest_configure(config):
global option
option = config.option
print "config.option.setup: ", config.option.setup
You have to create a fixture that extracts this value from pytest's request.
# content of conftest.py
import pytest
def pytest_addoption(parser):
parser.addoption("--setup", action="store", help="setup.")
#pytest.fixture
def setup_option(request):
return request.config.getoption("--setup")
# basic usage:
# content of test_anything.py
def test_that(setup_option):
print("setup_option: %s" % setup_option)

fabric different parameters in different hosts

Please tell me how can i execute fab-script in a list of hosts with the same command BUT with the different values of parameter.
Something like this:
from fabric.api import *
def command(parameter):
run ("command%s" % parameter)
and execute this. I dont now how. For example:
fab -H host1,host2,host3 command:param1 command:param2 command:param3
And Fabric performs the following:
command:param1 executed on host1
command:param2 executed on host2
command:param3 executed on host3
The way I do this is to parametize the tasks. In my case it's about Deployment to Dev, Test and Production.
fabfile.py:
from ConfigParser import ConfigParser
from fabric.tasks import Task
from fabric.api import execute, task
#task()
def build(**options):
"""Your build function"""
class Deploy(Task):
name = "dev"
def __init__(self, *args, **kwargs):
super(Deploy, self).__init__(*args, **kwargs)
self.options = kwargs.get("options", {})
def run(self, **kwargs):
options = self.options.copy()
options.update(**kwargs)
return execute(build, **options)
config = ConfigParser()
config.read("deploy.ini")
sections = [
section
for section in config.sections()
if section != "globals" and ":" not in section
]
for section in sections:
options = {"name": section}
options.update(dict(config.items("globals")))
options.update(dict(config.items(section)))
t = Deploy(name=section, options=options)
setattr(t, "__doc__", "Deploy {0:s} instance".format(section))
globals()[section] = task
deploy.ini:
[globals]
repo = https://github.com/organization/repo
[dev]
dev = yes
host = 192.168.0.1
domain = app.local
[prod]
version = 1.0
host = 192.168.0.2
domain = app.mydomain.tld
Hopefully this is obvious enough to see that you can configure all kinds of deployments by simply editing your deploy.ini configuration file and subsequently automatically ceacreating new tasks that match with the right parameters.
This pattern can be adapted to YAML or JSON if that's your "cup of tea'>

Optional parameter not working on schema

I am adding validation using schema for CLI that uses docopt, but I cannot seem to get optional to work. I want to validate that:
the input file exists
valid options are used
if the PATH is added that the directory exists.
Here is app so far
"""DVget
Usage:
DVget [-s] FILE [PATH]
Process a file, return data based on selection
and write results to PATH/output-file
Arguments:
FILE specify input file
PATH specify output directory (default: ./)
Options:
-s returns sections
-p returns name-sets
-m returns modules
"""
import os
from docopt import docopt
from schema import Schema, And, Use, Optional, SchemaError
# START OF SCRIPT
if __name__ == "__main__":
arguments = docopt(__doc__, version="0.1")
#print(arguments)
schema = Schema({
'FILE': [Use(open, error='FILE should be readable')],
Optional('PATH'): And(os.path.exists, error='PATH should exist'),
'-': And(str, lambda s: s in ('s', 'p', 'm'))})
try:
arguments = schema.validate(arguments)
# process(arguments)
except SchemaError as e:
exit(e)
running DVget -s "c:\test.txt" gives me the error message 'PATH should exist' even when using Optional in schema and docopt. Any suggestions?

Categories