Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[QUESTION] Is Command chaining supported? #107

Open
4 tasks done
atetevoortwis opened this issue May 25, 2020 · 6 comments
Open
4 tasks done

[QUESTION] Is Command chaining supported? #107

atetevoortwis opened this issue May 25, 2020 · 6 comments
Labels
question Question or problem

Comments

@atetevoortwis
Copy link

First check

  • I used the GitHub search to find a similar issue and didn't find it.
  • I searched the Typer documentation, with the integrated search.
  • I already searched in Google "How to X in Typer" and didn't find any information.
  • I already searched in Google "How to X in Click" and didn't find any information.

Description

Is it possible to 'chain' commands in a single Typer call? Typical usecase for us would be an application where we have a CLI app to do input->processing->output kind of tasks, and for all three there are multiple variants with different options, but they can be combined in a lot of ways.

Additional context

We currently use Google Fire for our CLI app (which I was surprised not to found in the Inspiration section), which does this by saving intermediate results in a class containing all 'Commands'.

@atetevoortwis atetevoortwis added the question Question or problem label May 25, 2020
@kalzoo
Copy link

kalzoo commented Jul 3, 2020

+1 (I think).

Edit: what I describe in my comment below is mostly a click problem, not a typer one! I had missed this fun discussion which pointed me the right way towards using click Context, and then the docs here became more relevant.

Solution, following the same example from [the docs] and my comments below:

import typer

app = typer.Typer

@app.callback()
def root(ctx: typer.Context, universe_name: str = typer.Option(None)):
    ctx.obj = universe_name

def universe_name_callback(ctx: typer.Context, value: Optional[str]):
    output = value or ctx.obj
    if not output:
        raise typer.BadParameter("--universe-name must be provided")
    return output

def conquer(universe_name: str = typer.Option(None, callback=universe_name_callback), reign_name: str = typer.Option(...)):
    typer.echo("Universe Name: " + universe_name)
    typer.echo("Reign Name: " + reign_name)

# this works
> python reigns.py --universe-name UNIVERSE_NAME conquer --reign-name REIGN_NAME

# this also works
> python reigns.py  conquer --reign-name REIGN_NAME --universe-name UNIVERSE_NAME

# this throws an error as expected
> python reigns.py  conquer --reign-name REIGN_NAME

However, this is clumsy and leaves plenty of room for error. It would still be nice if we could share options among different callbacks and commands, even if click is hostile to that idea.

I used ctx.obj for simplicity here but state would be no better. It meets the requirement for my use case so that's good. I could use click's pass decorators to do the rest, but since typer isn't compatible with those I'll leave it as is for now.

Hope this helps someone!


I'm packaging a CLI within a docker image, and it's important that I can configure the entrypoint with a root command and argument, but then that further arguments and commands can be appended as docker commands.

I've been all through the typer docs, which were a great intro - but without inline documentation in the code, the next step is to ask here.

For example:

Using the reigns example from your docs (loosely), imagine a command

python reigns.py UNIVERSE_NAME conquer REIGN_NAME

or something to that effect. I'd be just as happy with those as args or options - similar to how git itself allows mixing of args, flags, and options in almost any order. It does help that conquer remains a command, since that's the power of typer, otherwise we would just make everything an argument or option:

python reigns.py --universe_name UNIVERSE_NAME --operation conquer --reign_name REIGN_NAME

That discards much of what makes typer great and quick to set up, since everything is now packed into one root command. But it does allow me to draw a line between a static entrypoint and dynamic commands:

python reigns.py --universe_name UNIVERSE_NAME ||<entrypoint --- command>|| --operation conquer --reign_name REIGN_NAME

I did try just sending options out of order:

python reigns.py --universe_name UNIVERSE_NAME conquer REIGN_NAME
# Error: no such option: --universe_name

Putting it back in order fixed the error, but doesn't meet my docker ordering requirement:

# This works
python reigns.py  conquer REIGN_NAME --universe_name UNIVERSE_NAME

Best-case scenario, all commands in the tree would receive all options. Callbacks don't appear to serve this use case, since the option's value is needed by the command.

Is there a better way?

@JeromeK13
Copy link

JeromeK13 commented Aug 11, 2020

Hey Guys

Yes this can be activated like this:

app = typer.Typer(chain=True)

@pmsoltani
Copy link

@JeromeK13 would you please provide a more detailed example? How, for instance, do I name the command that executes other commands in sequence?

Appreciate your help in advance!

@JeromeK13
Copy link

JeromeK13 commented Sep 1, 2020

import typer

# Init CLI
cli = typer.Typer(chain=True)

@cli.command('my_command_name')
def my_function_name():
    typer.echo("Chain 1")

@cli.command('my_command_name2')
def my_function_name():
    typer.echo("Chain 2")

if __name__ == "__main__":
    cli()

So if you now run the CLI you can do the following (the command will execute in the order they provided)
python3 mycli.py my_command_name my_command_name2

@pmsoltani
Copy link

@JeromeK13 thanks for your prompt reply. I think perhaps "chaining commands" is not what I wanted. This is what I had in mind (using Click):

@click.command()
@click.option("--with-testdb/--no-with-testdb", default=False)
@click.pass_context
def reset(ctx):
    """Inits and seeds the database automatically."""
    ctx.invoke(init, with_testdb=with_testdb)
    ctx.invoke(seed)

    return None

After some fiddling with the code, I arrived at this:

@cli.command()
def reset(ctx: typer.Context, with_testdb: bool = False):
    """Inits and seeds the database automatically."""
    ctx.invoke(init, with_testdb)
    ctx.invoke(cyan)

Anyway, thanks again for your time.

@Nitinsiwach
Copy link

This should be made a part of the documentation. Great package otherwise

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Question or problem
Projects
None yet
Development

No branches or pull requests

5 participants