This is possible since Click 4.0. You need to tell click to ignore the unknown option, and that you want the options gathered up and passed in. Here is your example with the relevant changes made.
Code:
@click.group()
@click.option('--arg')
def cli(arg):
click.echo('cli arg: %s' % arg)
pass
@cli.command(context_settings=dict(
ignore_unknown_options=True,
help_option_names=[],
))
@click.argument('script')
@click.argument('args', nargs=-1, type=click.UNPROCESSED)
def run(script, args):
click.echo('script: %s' % script)
click.echo(args)
Test:
cli('--arg 3 run foo.py --arg 5 arg1 --arg2 --arg 4 --help'.split())
Produces:
cli arg: 3
script: foo.py
('--arg', '5', 'arg1', '--arg2', '"--arg', '4"', '--help')
(Source)
Answer from Stephen Rauch on Stack Overflowcommand line interface - Optional CLI arguments with python Click library option - Stack Overflow
python - Optional argument in command with click - Stack Overflow
python - Click command default arguments and/or options - Stack Overflow
python - How to pass several list of arguments to @click.option - Stack Overflow
One way that I have managed to achieve this behaviour was by actually using arguments as below. I'll post this as a workaround, while I try to see if it could be done with an option, and I'll update my post accordingly
@click.command(context_settings={"ignore_unknown_options": True})
@click.argument("options", nargs=-1)
def myscript(options):
option = False
if options is ():
option = False
if '-o' in options or '--option' in options:
option = True
if len(options) > 1:
option = options[1]
print(option)
Later Edit Using an option, I have managed to achieve this by adding an argument to the command definition.
@click.command()
@click.option('-o', '--option', is_flag=True, default=False, is_eager=True)
@click.argument('value', nargs=-1)
def myscript(option, value):
if option and value != ():
option = value[0]
print(option)
The nargs can be removed if you only expect at most one argument to follow, and can be treated as not required.
@click.command()
@click.option('-o', '--option', is_flag=True, default=False, is_eager=True)
@click.argument('value', required=False)
def myscript(option, value=None):
if option and value is not None:
option = value
print(option)
It might also be possible by putting together a context generator and storing some state, but that seems the least desirable solution, since you would be relying on the context storing your state.
FYI, if anybody bumps into this problem in 2024, it seems that click has made something specific for this use case, see: https://click.palletsprojects.com/en/8.1.x/options/#optional-value
Setting is_flag=False, flag_value=value tells Click that the option can still be passed a value, but if only the flag is given the flag_value is used.
@click.command()
@click.option("--name", is_flag=False, flag_value="Flag", default="Default")
def hello(name):
click.echo(f"Hello, {name}!")
>> hello
Hello, Default!
>> hello --name Value
Hello, Value!
>> hello --name
Hello, Flag!
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.
I was trying to figure this out..
How I did it was two ways.
- Default the value to something like a empty string. I prefer to give it a value for what I was doing since I had code reading that variable.
@click.argument( "myarg", default="", )
- Other way was to say it is not required.
@click.argument( "myarg", required=False, )
If you don't insist on passing something that looks like a list, but simply want to pass multiple variadic arguments, you can use the multiple option.
From the click documentation
@click.command()
@click.option('--message', '-m', multiple=True)
def commit(message):
click.echo('\n'.join(message))
$ commit -m foo -m bar
foo
bar
You can coerce click into taking multiple list arguments, if the lists are formatted as a string literals of python lists by using a custom option class like:
Custom Class:
import click
import ast
class PythonLiteralOption(click.Option):
def type_cast_value(self, ctx, value):
try:
return ast.literal_eval(value)
except:
raise click.BadParameter(value)
This class will use Python's Abstract Syntax Tree module to parse the parameter as a python literal.
Custom Class Usage:
To use the custom class, pass the cls parameter to @click.option() decorator like:
@click.option('--option1', cls=PythonLiteralOption, default=[])
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 the desired methods.
In this case we over ride click.Option.type_cast_value() and then call ast.literal_eval() to parse the list.
Test Code:
@click.command(context_settings=dict(help_option_names=['-h', '--help']))
@click.option('--option1', cls=PythonLiteralOption, default=[])
@click.option('--option2', cls=PythonLiteralOption, default=[])
def cli(option1, option2):
click.echo("Option 1, type: {} value: {}".format(
type(option1), option1))
click.echo("Option 2, type: {} value: {}".format(
type(option2), option2))
# do stuff
if __name__ == '__main__':
import shlex
cli(shlex.split(
'''--option1 '["o11", "o12", "o13"]'
--option2 '["o21", "o22", "o23"]' '''))
Test Results:
Option 1, type: <type 'list'> value: ['o11', 'o12', 'o13']
Option 2, type: <type 'list'> value: ['o21', 'o22', 'o23']
import sys
import click
import pandas as pd
from sklearn import metrics
@click.command()
# @click.option('--filename')
## @click.argument('result.csv', type=click.Path(exists=True))
@click.argument('filename', type=click.Path(exists=True))
def main(filename):
df = pd.read_csv(filename)
y_test = df["actual"].values
y_pred = df["predicted"].values
print('R Squared: ' + str(metrics.r2_score(y_test, y_pred)))
if __name__ == '__main__':
# sys.argv = ['', '--filename', 'result.csv']
sys.argv = ['', 'result.csv']
main()
Instead of a named option @click.option('--filename') you can use the argument decorator and specifying:
- the parameter-name of your function like here
filenameand - the type of the parameter, e.g. a file-path that is validated for existence:
click.Path(exists=True)
Click Argument for file-paths
You can define File Path Arguments as follows with click:
@click.command()
@click.argument('filename', type=click.Path(exists=True))
def main(filename):
print(f"{filename=}")
df = pd.read_csv(filename)
However this means changing your command-line, e.g. by removing the option '--filename' element from the array:
if __name__ == '__main__':
sys.argv = ['', 'result.csv']
main()
Attribute Error
The runtime error mentioned in a comment to guolei's answer:
AttributeError: 'tuple' object has no attribute 'tb_frame'
is most likely not related to your presented Python script (incl. click, pandas or scikit-learn). It rather comes from your runtime-environment like IPython or Jupyter Notebook. See AttributeError: 'TypeError' object has no attribute 'tb_frame' bug ยท Issue #12877 ยท ipython/ipython.
The Forwarding Unknown Options section in the documentation covers this use case. You should be able to write something like this:
@click.command(context_settings=dict(
ignore_unknown_options=True,
))
@click.option("--port", type=int)
@click.argument("args", nargs=-1, type=click.UNPROCESSED)
def main(port, args):
call_the_wrapped_program(port=port, args=args)
Assuming you invoke the program with something akin to a cli command, have you tried simply calling it like this?
cli --port 8080 -- -k arg1 arg2 -r etc
If I print args with that invocation, I get all the arguments out, albeit as a string, but I'm hoping that whatever third party you want to delegate to might be able to run its own parsing over that.
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.