Reputation: 1140
The click
library discourages invoking the click.forward
and click.forward
functions to 'self-invoke' CLI commands. To quote 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.
However, they do not offer alternatives.
For example, let's say we want to use our own CLI's call our_cli get user-funds
to verify a user's funds before we execute the hypothetical our_cli buy some_item
.
How would be the way to do that without using Context.invoke
or Context.forward
?
PS: This is not a question about using the invoke
and forward
functions. That has already been discussed over here: link, link.
Upvotes: 1
Views: 1495
Reputation: 1140
Thanks to @StephenRauch for the tip of calling the python code directly.
Here is a simplified example that shows how to refactor a Context.invoke
call to call python code directly.
Assume that we invoked our CLI's get-user-funds
command to get a user's budget before we buy a hypothetical item.
import click
# E.g., assume the price and funds come from some API
def get_funds(user): return 100
def get_price(item): return 50
@click.group()
@click.pass_context
def our_cli(ctx):
# To simplify, assume that the CLI already knows the relevant user.
ctx.obj = {"user": "Cindy"}
@our_cli.command()
@click.argument("user")
def get_user_funds(user):
# Normally we would use this command to print the funds
# of a user to stdout.
funds = get_funds(user)
click.echo(f"{funds=}")
return funds
@our_cli.command()
@click.argument("item")
@click.pass_context
def buy_item(ctx, item):
# This is the `invoke` call that we wish to refactor.
funds = ctx.invoke(get_user_funds)
if funds >= get_price(item):
print(f"bought {item}")
else:
print("f{funds}")
if __name__ == "__main__":
our_cli()
Instead of calling Context.invoke
to get the funds, we can also call the python code directly.
We can do that by rewriting buy_item
as follows:
@our_cli.command()
@click.argument("item")
@click.pass_context
def buy_item(ctx: click.Context, item: str):
# Now we call python code directly.
funds = get_funds(ctx.obj["user"])
# Note that bypass the click.echo(f"{funds=}") call now. This
# is probably something we would like in this example.
if funds >= get_price(item):
print(f"bought {item}")
else:
print("f{funds}")
In this example, the refactoring was very simple.
We already had a python function (i.e. get_funds
) that we could call directly.
When working with more complex code, it is likely that you have to restructure your code.
In my case, among other things, I had to extract the logic that I wanted to call directly from a @click.command
annotated function to a normal python function.
After that, I was able to replace the Context.invoke
call with a direct function call.
Upvotes: 1