When you call add_name() and add_surname() directly from another function, you actually call the decorated versions of them so the arguments expected may not be as you defined them (see the answers to How to strip decorators from a function in python for some details on why).
I would suggest modifying your implementation so that you keep the original functions undecorated and create thin click-specific wrappers for them, for example:
def add_name(content, to_stdout=False):
if not content:
content = ''.join(sys.stdin.readlines())
result = content + "\n\tadded name"
if to_stdout is True:
sys.stdout.writelines(result)
return result
@click.command()
@click.argument('content', required=False)
@click.option('--to_stdout', default=True)
def add_name_command(content, to_stdout=False):
return add_name(content, to_stdout)
You can then either call these functions directly or invoke them via a CLI wrapper script created by setup.py.
This might seem redundant but in fact is probably the right way to do it: one function represents your business logic, the other (the click command) is a "controller" exposing this logic via command line (there could be, for the sake of example, also a function exposing the same logic via a Web service for example).
In fact, I would even advise to put them in separate Python modules - Your "core" logic and a click-specific implementation which could be replaced for any other interface if needed.
Answer from shevron on Stack OverflowWhen you call add_name() and add_surname() directly from another function, you actually call the decorated versions of them so the arguments expected may not be as you defined them (see the answers to How to strip decorators from a function in python for some details on why).
I would suggest modifying your implementation so that you keep the original functions undecorated and create thin click-specific wrappers for them, for example:
def add_name(content, to_stdout=False):
if not content:
content = ''.join(sys.stdin.readlines())
result = content + "\n\tadded name"
if to_stdout is True:
sys.stdout.writelines(result)
return result
@click.command()
@click.argument('content', required=False)
@click.option('--to_stdout', default=True)
def add_name_command(content, to_stdout=False):
return add_name(content, to_stdout)
You can then either call these functions directly or invoke them via a CLI wrapper script created by setup.py.
This might seem redundant but in fact is probably the right way to do it: one function represents your business logic, the other (the click command) is a "controller" exposing this logic via command line (there could be, for the sake of example, also a function exposing the same logic via a Web service for example).
In fact, I would even advise to put them in separate Python modules - Your "core" logic and a click-specific implementation which could be replaced for any other interface if needed.
Due to the click decorators the functions can no longer be called just by specifying the arguments. The Context class is your friend here, specifically:
- Context.invoke() - invokes another command with the arguments you supply
- Context.forward() - fills in the arguments from the current command
So your code for add_name_and_surname should look like:
@click.command()
@click.argument('content', required=False)
@click.option('--to_stdout', default=False)
@click.pass_context
def add_name_and_surname(ctx, content, to_stdout=False):
result = ctx.invoke(add_surname, content=ctx.forward(add_name))
if to_stdout is True:
sys.stdout.writelines(result)
return result
Reference: http://click.pocoo.org/6/advanced/#invoking-other-commands
python - Call a click command from code - Stack Overflow
Invoke python click command with custom class from another command - Stack Overflow
Question: How to invoke subcommands directly from the main Click Group?
How do I manually invoke click CLIs from within a Python calling context with parameters? - Stack Overflow
I tried with Python 3.7 and Click 7 the following code:
import click
@click.command()
@click.option('-w', '--width', type=int, default=0)
@click.option('--option2')
@click.argument('argument')
def app(width, option2, argument):
click.echo("params: {} {} {}".format(width, option2, argument))
assert width == 3
assert option2 == '4'
assert argument == 'arg'
app(["arg", "--option2", "4", "-w", 3], standalone_mode=False)
app(["arg", "-w", 3, "--option2", "4" ], standalone_mode=False)
app(["-w", 3, "--option2", "4", "arg"], standalone_mode=False)
All the app calls are working fine!
This use-case is described in the docs.
Sometimes, it might be interesting to invoke one command from another command. This is a pattern that is generally discouraged with Click, but possible nonetheless. For this, you can use the Context.invoke() or Context.forward() methods.
cli = click.Group()
@cli.command()
@click.option('--count', default=1)
def test(count):
click.echo('Count: %d' % count)
@cli.command()
@click.option('--count', default=1)
@click.pass_context
def dist(ctx, count):
ctx.forward(test)
ctx.invoke(test, count=42)
They work similarly, but the difference is that Context.invoke() merely invokes another command with the arguments you provide as a caller, whereas Context.forward() fills in the arguments from the current command.
I think the Multi Command Chaining and Multi Command Pipelines features are designed to cover this situation. Pipelining gives the exact behaviour requested (both step1 and step2 invoked when nothing is given on the command line), but it's more verbose, and with chain=True none of the arguments can be optional; you must either (a) always give both arg1 and arg2, even when invoking only step2; or (b) convert these arguments to options (--arg1 foo instead of foo).
Chaining
import click
@click.group(chain=True)
def cli():
pass
@cli.command()
@click.argument('arg1')
def step1(arg1):
click.echo('Step 1: ' + arg1)
@cli.command()
@click.argument('arg2')
def step2(arg2):
click.echo('Step 2: ' + arg2)
cli()
Then:
$ python3 chain.py step1 foo step2 bar
Step 1: foo
Step 2: bar
$ python3 chain.py step2 bar
Step 2: bar
Pipelining
import click
@click.group(chain=True, invoke_without_command=True)
@click.argument('arg1')
@click.argument('arg2')
def cli(arg1, arg2):
pass
@cli.resultcallback()
def process_pipeline(processors, **kwargs):
# If no commands given, invoke step1 then step2
processors = processors if len(processors) else [step1, step2]
for processor in processors:
processor(**kwargs)
def step1(**kwargs):
click.echo('Step 1: ' + kwargs['arg1'])
def step2(**kwargs):
click.echo('Step 2: ' + kwargs['arg2'])
@cli.command('step1')
def make_step1():
return step1
@cli.command('step2')
def make_step2():
return step2
cli()
Then
$ python3 pipeline.py foo bar
Step 1: foo
Step 2: bar
$ python3 pipeline.py foo bar step2
Step 2: bar
I didn't phrase the question to fully explain what I was trying to do, since each step also requires the output from the previous one. Huge thanks to Paul for starting me on the right path.
My solution was something like:
@click.group(invoke_without_command=True)
@click.option('--arg1')
@click.option('--arg2')
@click.pass_context
def cli(ctx, arg1, arg2):
'''Description
'''
if ctx.invoked_subcommand is None:
do_everything(ctx, arg1, arg2)
@cli.command()
@click.option('--arg1')
def step_1(arg1):
return do_something(arg1)
@cli.command()
@click.argument('step_one_result')
@click.option('--arg2')
def step_2(step_one_result, arg2):
do_something_else(step_one_result, arg2)
def do_everything(ctx, arg1, arg2):
step_one_result = ctx.invoke(step_1, arg1=arg1)
ctx.invoke(do_something_else, step_one_result=step_one_result, arg2=arg2)
#and because of weirdness with pass_context and using setuptools
def main():
cli(obj={})
if __name__ == '__main__':
main()
EDIT: you'll notice the use of ctx.invoke() which was necessary to call the functions without getting the following error
line 619, in make_context
ctx = Context(self, info_name=info_name, parent=parent, **extra)
TypeError: __init__() got an unexpected keyword argument 'arg1'