melvio
melvio

Reputation: 1140

How to 'self-invoke' python-click CLI commands without Context.invoke or Context.forward?

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

Answers (1)

melvio
melvio

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.

Example

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()

Refactoring

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}")

Closing Remarks

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

Related Questions