typer icon indicating copy to clipboard operation
typer copied to clipboard

[FEATURE] [BUG] Behavior of app with single command is inconsistent if added to another app

Open graue70 opened this issue 3 years ago • 6 comments

I'm not sure whether this is a bug or a feature request. I think the behavior is not documented, so it might be a bug.

Related problem

In the docs, it says that when adding only one command to an app, 'Typer is smart enough to create a CLI application with that single function as the main CLI application, not as a command/subcommand' (no click group is created). See this example, which can be run with python welcome.py (leaving out the name of the command main):

# welcome.py
import typer


app = typer.Typer()


@app.command()
def main():
    typer.echo("Hello World!")


if __name__ == "__main__":
    app()

When I add this typer app to another app, the behavior changes. I add this file:

# parent.py
import typer

import welcome


app = typer.Typer()
app.add_typer(welcome.app, name="welcome")


@app.command()
def main():
    typer.echo("Hello parent app!")


if __name__ == "__main__":
    app()

When I run python parent.py welcome, I get the following output:

Usage: parent.py welcome [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  main

The solution you would like

I expect the behavior of a typer app to stay consistent, no matter whether it is the top-level-app or a sub-app of another one. In the example above, I expect the output to be Hello world!, but in order to get that, I need to run python parent.py welcome main instead.

Describe alternatives you've considered

There is a workaround to get the desired behavior: Replace the line app.add_typer(welcome.app, name="welcome") by app.command(name="welcome")(welcome.main) in the file parent.py. However, this doesn't solve the inconsistency and it's also not very flexible. As soon as a second command is added to welcome.py, the code needs to be changed back.

graue70 avatar Mar 04 '21 14:03 graue70

This would indeed be really useful as single commands of a typer.Typer instance could then live in separate files.

I also agree with you when it comes to the workaround you described and would like to add: With the workaround, the decorator app.command() doesn't live in the same file as the implementation welcome.main – causing the two to be weirdly disconnected, even though they clearly belong together. (Consider that welcome.main's signature might contain typer.Arguments and typer.Options that might e.g. add help texts – just like app.command().)

codethief avatar Mar 04 '21 15:03 codethief

+1 ran into the same. Also thanks for the workaround

pkkr avatar Apr 30 '21 16:04 pkkr

Just ran into this issue. I kept searching the documentation and online before I remembered to search here, and found this. It would at least be good to explicitly mention this behavior in the documentation, if not change it.

cyphase avatar Feb 08 '22 08:02 cyphase

(Disclaimer: I'm brand-new to typer (as of last night).)

#119 is also relevant to this.

The approach by @cataerogong in this comment is similar to what I'm using. The relevant part of that comment roughly being:

import child

app = typer.Typer()

if len(child.app.registered_commands) == 1:
    app.command('child_name')(child.app.registered_commands[0].callback)
else:
    app.add_typer(child.app, name='child_name')

(Note: I haven't yet considered how run-time changes might affect this approach.)

For now, I monkeypatched typer.Typer.add_typer so that it essentially does something very similar to the above (modulo how the child's name is set). Though I would prefer it if typer had, built-in, at least an option to have this kind of behavior--so that it can still be backward-compatible with how people might have come to expect add_typer() behavior for such single command sub-apps, but optionally mirror the semantics that the docs describe for single command apps in general (i.e. if the (sub-)app has only 1 command, there's no need to include the command name when executing it). From limited testing, this approach works as expected.

(@tiangolo gives another solution here which I had already considered, but my implementation was buggin' out when I would pass e.g. an argument to the sub-app. I haven't spent enough time to see if it's a bug with my code or something else, but his alternative is probably also worth trying.)

single-fingal avatar Nov 18 '22 05:11 single-fingal

sorry for this +1 reply but just ran into exactly the same issue.

haoyun avatar Jan 16 '24 00:01 haoyun

Struggled with this for a while thank you for this, I would be happy for this to exist in the docs under sub-commands.

albalamia avatar Aug 02 '24 06:08 albalamia