diff --git a/docs/src/commands/tutorial003.py b/docs/src/commands/arguments/tutorial001.py similarity index 100% rename from docs/src/commands/tutorial003.py rename to docs/src/commands/arguments/tutorial001.py diff --git a/docs/src/commands/tutorial005.py b/docs/src/commands/help/tutorial001.py similarity index 100% rename from docs/src/commands/tutorial005.py rename to docs/src/commands/help/tutorial001.py diff --git a/docs/src/commands/help/tutorial002.py b/docs/src/commands/help/tutorial002.py new file mode 100644 index 0000000..d098d23 --- /dev/null +++ b/docs/src/commands/help/tutorial002.py @@ -0,0 +1,23 @@ +import typer + +app = typer.Typer() + + +@app.command(help="Create a new user with USERNAME.") +def create(username: str): + """ + Some internal utility function to create. + """ + typer.echo(f"Creating user: {username}") + + +@app.command(help="Delete a user with USERNAME.") +def delete(username: str): + """ + Some internal utility function to delete. + """ + typer.echo(f"Deleting user: {username}") + + +if __name__ == "__main__": + app() diff --git a/docs/src/commands/tutorial001.py b/docs/src/commands/index/tutorial001.py similarity index 100% rename from docs/src/commands/tutorial001.py rename to docs/src/commands/index/tutorial001.py diff --git a/docs/src/commands/tutorial002.py b/docs/src/commands/index/tutorial002.py similarity index 100% rename from docs/src/commands/tutorial002.py rename to docs/src/commands/index/tutorial002.py diff --git a/docs/src/commands/tutorial006.py b/docs/src/commands/name/tutorial001.py similarity index 100% rename from docs/src/commands/tutorial006.py rename to docs/src/commands/name/tutorial001.py diff --git a/docs/src/commands/tutorial004.py b/docs/src/commands/options/tutorial001.py similarity index 100% rename from docs/src/commands/tutorial004.py rename to docs/src/commands/options/tutorial001.py diff --git a/docs/src/options/tutorial001.py b/docs/src/options/help/tutorial001.py similarity index 100% rename from docs/src/options/tutorial001.py rename to docs/src/options/help/tutorial001.py diff --git a/docs/src/options/tutorial006.py b/docs/src/options/help/tutorial002.py similarity index 100% rename from docs/src/options/tutorial006.py rename to docs/src/options/help/tutorial002.py diff --git a/docs/src/options/name/tutorial001.py b/docs/src/options/name/tutorial001.py new file mode 100644 index 0000000..7bc212f --- /dev/null +++ b/docs/src/options/name/tutorial001.py @@ -0,0 +1,9 @@ +import typer + + +def main(user_name: str = typer.Option(..., "--name")): + typer.echo(f"Hello {user_name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs/src/options/name/tutorial002.py b/docs/src/options/name/tutorial002.py new file mode 100644 index 0000000..10eba64 --- /dev/null +++ b/docs/src/options/name/tutorial002.py @@ -0,0 +1,9 @@ +import typer + + +def main(user_name: str = typer.Option(..., "--name", "-n")): + typer.echo(f"Hello {user_name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs/src/options/name/tutorial003.py b/docs/src/options/name/tutorial003.py new file mode 100644 index 0000000..5a8325a --- /dev/null +++ b/docs/src/options/name/tutorial003.py @@ -0,0 +1,9 @@ +import typer + + +def main(user_name: str = typer.Option(..., "-n")): + typer.echo(f"Hello {user_name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs/src/options/name/tutorial004.py b/docs/src/options/name/tutorial004.py new file mode 100644 index 0000000..fa31904 --- /dev/null +++ b/docs/src/options/name/tutorial004.py @@ -0,0 +1,9 @@ +import typer + + +def main(user_name: str = typer.Option(..., "--user-name", "-n")): + typer.echo(f"Hello {user_name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs/src/options/name/tutorial005.py b/docs/src/options/name/tutorial005.py new file mode 100644 index 0000000..d0a094d --- /dev/null +++ b/docs/src/options/name/tutorial005.py @@ -0,0 +1,15 @@ +import typer + + +def main( + name: str = typer.Option(..., "--name", "-n"), + formal: bool = typer.Option(False, "--formal", "-f"), +): + if formal: + typer.echo(f"Good day Ms. {name}.") + else: + typer.echo(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs/src/options/tutorial003.py b/docs/src/options/prompt/tutorial001.py similarity index 100% rename from docs/src/options/tutorial003.py rename to docs/src/options/prompt/tutorial001.py diff --git a/docs/src/options/tutorial004.py b/docs/src/options/prompt/tutorial002.py similarity index 100% rename from docs/src/options/tutorial004.py rename to docs/src/options/prompt/tutorial002.py diff --git a/docs/src/options/tutorial005.py b/docs/src/options/prompt/tutorial003.py similarity index 100% rename from docs/src/options/tutorial005.py rename to docs/src/options/prompt/tutorial003.py diff --git a/docs/src/options/tutorial002.py b/docs/src/options/required/tutorial001.py similarity index 100% rename from docs/src/options/tutorial002.py rename to docs/src/options/required/tutorial001.py diff --git a/docs/tutorial/arguments.md b/docs/tutorial/arguments.md index adae09d..8915ed9 100644 --- a/docs/tutorial/arguments.md +++ b/docs/tutorial/arguments.md @@ -1,4 +1,4 @@ -The same way that you have `typer.Option()` to help you define things for *CLI options*, there's also the equivalent `typer.Argument()` for *CLI arguments*. +Let's see how to configure *CLI arguments* with `typer.Argument()`. ## Optional *CLI arguments* @@ -63,20 +63,15 @@ And then we changed it to: name: str = typer.Argument(...) ``` -The same as with `typer.Option()`, there is a `typer.Argument()`. +But now as `typer.Argument()` is the "default value" of the function's parameter, it would mean that "it is no longer required" (in Python terms). -And now as `typer.Argument()` is the "default value" of the function's parameter, in Python terms, it would mean that "it is no longer required" (in Python terms). +As we no longer have the Python function default value (or its absence) to tell if something is required or not and what is the default value, the first parameter to `typer.Argument()` serves the same purpose of defining that default value, or making it required. -As we no longer have the Python function default value (or its absence) to tell it if something is required or not and what is the default value, the first parameter to `typer.Argument()` serves the same purpose of defining that default value, or making it required. - -To make it *required*, we pass `...` as that first parameter to the function. +To make it *required*, we pass `...` as the first function argument passed to `typer.Argument(...)`. !!! info If you hadn't seen that `...` before: it is a a special single value, it is part of Python and is called "Ellipsis". -!!! tip - This works exactly the same way `typer.Option()` does. - All we did there achieves the same thing as before, a **required** *CLI argument*:
@@ -150,7 +145,7 @@ $ python main.py Hello World! // With one optional CLI argument -$ python main.py +$ python main.py Camila Hello Camila ``` diff --git a/docs/tutorial/commands.md b/docs/tutorial/commands.md deleted file mode 100644 index 446c1d8..0000000 --- a/docs/tutorial/commands.md +++ /dev/null @@ -1,555 +0,0 @@ -We have seen how to create a CLI program with possibly several *CLI Options* and *CLI Arguments*. - -But **Typer** allows you to create CLI programs with several commands (also known as subcommands). - -For example, the program `git` has several commands. - -One command of `git` is `git push`. And `git push` in turn takes its own *CLI arguments* and *CLI options*. - -For example: - -
- -```console -// The push command with no parameters -$ git push - ----> 100% - -// The push command with one CLI option --set-upstream and 2 CLI arguments -$ git push --set-upstream origin master - ----> 100% -``` - -
- -Another command of `git` is `git pull`, it also has some *CLI parameters*. - -It's like if the same big program `git` had several small programs inside. - -!!! tip - A command looks the same as a *CLI argument*, it's just some name without a preceding `--`. But commands have predefined name, and are used to group different sets of functionalities into the same CLI application. - -## Command or subcommand - -It's common to call a CLI program a "command". - -But when one of these programs have subcommands, those subcommands are also frequently called just "commands". - -Have that in mind so you don't get confused. - -Here I'll use **CLI application** or **program** to refer to the program you are building in Python with Typer, and **command** to refer to one of these "subcommands" of your program. - -## Explicit application - -Before creating CLI applications with multiple commands/subcommands we need to understand how to create an explicit `typer.Typer()` application. - -In the *CLI Options* and *CLI Argument* tutorials you have seen how to create a single function and then pass that function to `typer.run()`. - -For example: - -```Python hl_lines="9" -{!./src/first_steps/tutorial002.py!} -``` - -But that is actually a shortcut. Under the hood, **Typer** converts that to a CLI application with `typer.Typer()` and executes it. All that inside of `typer.run()`. - -There's also a more explicit way to achieve the same: - -```Python hl_lines="3 6 12" -{!./src/commands/tutorial001.py!} -``` - -When you use `typer.run()`, **Typer** is doing more or less the same as above, it will: - -* Create a new `typer.Typer()` "application". -* Create a new "`command`" with your function. -* Call the same "application" as if it was a function with "`app()`". - -!!! info "`@decorator` Info" - That `@something` syntax in Python is called a "decorator". - - You put it on top of a function. Like a pretty decorative hat (I guess that's where the term came from). - - A "decorator" takes the function below and does something with it. - - In our case, this decorator tells **Typer** that the function below is a "`command`". - -Both ways, with `typer.run()` and creating the explicit application, achieve the same. - -!!! tip - If your use case is solved with just `typer.run()`, that's fine, you don't have to create the explicit `app` and use `@app.command()`, etc. - - You might want to do that later when your app needs the extra features, but if it doesn't need them yet, that's fine. - -If you run the second example, with the explicit `app`, it works exactly the same: - -
- -```console -// Without a CLI argument -$ python main.py - -Usage: main.py [OPTIONS] NAME -Try "main.py --help" for help. - -Error: Missing argument "NAME". - -// With the NAME CLI argument -$ python main.py Camila - -Hello Camila - -// Asking for help -$ python main.py --help - -Usage: main.py [OPTIONS] NAME - -Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. - --help Show this message and exit. -``` - -
- -## A CLI application with multiple commands - -Coming back to the CLI applications with multiple commands/subcommands, **Typer** allows creating CLI applications with multiple of them. - -Now that you know how to create an explicit `typer.Typer()` application and add one command, let's see how to add multiple commands. - -Let's say that we have a CLI application to manage users. - -We'll have a command to `create` users and another command to `delete` them. - -To begin, let's say it can only create and delete one single predefined user: - -```Python hl_lines="6 11" -{!./src/commands/tutorial002.py!} -``` - -Now we have a CLI application with 2 commands, `create` and `delete`: - -
- -```console -// Check the help -$ python main.py --help - -Usage: main.py [OPTIONS] COMMAND [ARGS]... - -Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. - --help Show this message and exit. - -Commands: - create - delete - -// Test them -$ python main.py create - -Creating user: Hiro Hamada - -$ python main.py delete - -Deleting user: Hiro Hamada - -// Now we have 2 commands! šŸŽ‰ -``` - -
- -Notice that the help text now shows the 2 commands: `create` and `delete`. - -!!! tip - By default, the names of the commands are generated from the function name. - -## Command *CLI arguments* - -The same way as with a CLI application with a single command, subcommands (or just "commands") can also have their own *CLI arguments*: - -```Python hl_lines="7 12" -{!./src/commands/tutorial003.py!} -``` - -
- -```console -// Check the help for create -$ python main.py create --help - -Usage: main.py create [OPTIONS] USERNAME - -Options: - --help Show this message and exit. - -// Call it with a CLI argument -$ python main.py create Camila - -Creating user: Camila - -// The same for delete -$ python main.py delete Camila - -Deleting user: Camila -``` - -
- -!!! tip - Everything to the *right* of the *command* are *CLI parameters* (*CLI arguments* and *CLI options*) for that command. - -!!! note "Technical Details" - Actually, it's everything to the right of that command, *before any subcommand*. - - It's possible to have groups of *subcommands*, it's like if one *command* also had *subcommands*. And then those *subcommands* could have their own *CLI parameters*, taking their own *CLI parameters*. - - You will see about them later in another section. - -## Command *CLI options* - -Commands can also have their own *CLI options*. - -In fact, each command can have different *CLI arguments* and *CLI options*: - -```Python hl_lines="7 13 14 24 33" -{!./src/commands/tutorial004.py!} -``` - -Here we have multiple commands, with different *CLI parameters*: - -* `create`: - * `username`: a *CLI argument*. -* `delete`: - * `username`: a *CLI argument*. - * `--force`: a *CLI option*, if not provided, it's prompted. -* `delete-all`: - * `--force`: a *CLI option*, if not provided, it's prompted. -* `init`: - * Doesn't take any *CLI parameters*. - -
- -```console -// Check the help -python main.py --help - -Usage: main.py [OPTIONS] COMMAND [ARGS]... - -Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. - --help Show this message and exit. - -Commands: - create - delete - delete-all - info -``` - -
- -!!! tip - Check the command `delete-all`, by default command names are generated from the function name, replacing `_` with `-`. - -Test it: - -
- -```console -// Check the command create -$ python main.py create Camila - -Creating user: Camila - -// Now test the command delete -$ python main.py delete Camila - -# Are you sure you want to delete the user? [y/N]: $ y - -Deleting user: Camila - -$ python main.py delete Wade - -# Are you sure you want to delete the user? [y/N]: $ n - -Operation cancelled - -// And finally, the command delete-all -// Notice it doesn't have CLI arguments, only a CLI option - -$ python main.py delete-all - -# Are you sure you want to delete ALL users? [y/N]: $ y - -Deleting all users - -$ python main.py delete-all - -# Are you sure you want to delete ALL users? [y/N]: $ n - -Operation cancelled - -// And if you pass the --force CLI option, it doesn't need to confirm - -$ python main.py delete-all --force - -Deleting all users - -// And init that doesn't take any CLI parameter -$ python main.py init - -Initializing user database -``` - -
- -## Command help - -The same as before, you can add help for the commands in the docstrings and the *CLI options*. - -And the `typer.Typer()` application receives a parameter `help` that you can pass with the main help text for your CLI program: - -```Python hl_lines="3 8 9 10 20 23 24 25 26 27 39 42 43 44 45 46 55 56 57" -{!./src/commands/tutorial005.py!} -``` - -Check it: - -
- -```console -// Check the new help -$ python main.py --help - -Usage: main.py [OPTIONS] COMMAND [ARGS]... - - Awesome CLI user manager. - -Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. - --help Show this message and exit. - -Commands: - create Create a new user with USERNAME. - delete Delete a user with USERNAME. - delete-all Delete ALL users in the database. - init Initialize the users database. - -// Now the commands have inline help šŸŽ‰ - -// Check the help for create -$ python main.py create --help - -Usage: main.py create [OPTIONS] USERNAME - - Create a new user with USERNAME. - -Options: - --help Show this message and exit. - -// Check the help for delete -$ python main.py delete --help - -Usage: main.py delete [OPTIONS] USERNAME - - Delete a user with USERNAME. - - If --force is not used, will ask for confirmation. - -Options: - --force / --no-force Force deletion without confirmation. [required] - --help Show this message and exit. - -// Check the help for delete-all -$ python main.py delete-all --help - -Usage: main.py delete-all [OPTIONS] - - Delete ALL users in the database. - - If --force is not used, will ask for confirmation. - -Options: - --force / --no-force Force deletion without confirmation. [required] - --help Show this message and exit. - -// Check the help for init -$ python main.py init --help - -Usage: main.py init [OPTIONS] - - Initialize the users database. - -Options: - --help Show this message and exit. -``` - -
- -!!! tip - `typer.Typer()` receives several other parameters for other things, we'll see that later. - - You will also see how to use "Callbacks" later, and those include a way to add this same help message in a function docstring. - -## Custom command name - -By default, the command names are generated from the function name. - -So, if your function is something like: - -```Python -def create(username: str): - ... -``` - -Then the command name will be `create`. - -But if you already had a function called `create()` somewhere in your code, you would have to name your CLI function differently. - -And what if you wanted the command to still be named `create`? - -For this, you can set the name of the command in the first parameter for the `@app.command()` decorator: - -```Python hl_lines="6 11" -{!./src/commands/tutorial006.py!} -``` - -Now, even though the functions are named `cli_create_user()` and `cli_delete_user()`, the commands will still be named `create` and `delete`: - -
- -```console -$ python main.py --help - -Usage: main.py [OPTIONS] COMMAND [ARGS]... - -Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. - --help Show this message and exit. - -Commands: - create - delete - -// Test it -$ python main.py create Camila - -Creating user: Camila -``` - -
- -## One command vs multiple commands - -You might have noticed that if you create a single command, as in the first example: - -```Python hl_lines="3 6 12" -{!./src/commands/tutorial001.py!} -``` - -**Typer** is smart enough to create a CLI application with that single function as the main CLI application, not as a command/subcommand: - -
- -```console -// Without a CLI argument -$ python main.py - -Usage: main.py [OPTIONS] NAME -Try "main.py --help" for help. - -Error: Missing argument "NAME". - -// With the NAME CLI argument -$ python main.py Camila - -Hello Camila - -// Asking for help -$ python main.py - -Usage: main.py [OPTIONS] NAME - -Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. - --help Show this message and exit. -``` - -
- -!!! tip - Notice that it doesn't show a command `main`, even though the function name is `main`. - -But if you add multiple commands, **Typer** will create one *CLI command* for each one of them: - -```Python hl_lines="6 11" -{!./src/commands/tutorial002.py!} -``` - -Here we have 2 commands `create` and `delete`: - -
- -```console -// Check the help -$ python main.py --help - -Usage: main.py [OPTIONS] COMMAND [ARGS]... - -Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. - --help Show this message and exit. - -Commands: - create - delete - -// Test the commands -$ python main.py create - -Creating user: Hiro Hamada - -$ python main.py delete - -Deleting user: Hiro Hamada -``` - -
- -!!! tip - This is **Typer**'s behavior by default, but you could still create a CLI application with one single command, like: - - ``` - $ python main.py main - ``` - - instead of just: - - ``` - $ python main.py - ``` - - You will learn more about this in the section about application Callbacks. - -## Decorator Technical Details - -When you use `@app.command()` the function under the decorator is registered in the **Typer** application and is then used later by the application. - -But Typer doesn't modify that function itself, the function is left as is. - -That means that if your function is simple enough that you could create it without using `typer.Option()` or `typer.Argument()`, you could use the same function for a **Typer** application and a **FastAPI** application putting both decorators on top, or similar tricks. - -!!! note "Click Technical Details" - This behavior is a design difference with Click. - - In Click, when you add a `@click.command()` decorator it actually modifies the function underneath and replaces it with an object. diff --git a/docs/tutorial/commands/arguments.md b/docs/tutorial/commands/arguments.md new file mode 100644 index 0000000..10052ef --- /dev/null +++ b/docs/tutorial/commands/arguments.md @@ -0,0 +1,39 @@ +The same way as with a CLI application with a single command, subcommands (or just "commands") can also have their own *CLI arguments*: + +```Python hl_lines="7 12" +{!./src/commands/arguments/tutorial001.py!} +``` + +
+ +```console +// Check the help for create +$ python main.py create --help + +Usage: main.py create [OPTIONS] USERNAME + +Options: + --help Show this message and exit. + +// Call it with a CLI argument +$ python main.py create Camila + +Creating user: Camila + +// The same for delete +$ python main.py delete Camila + +Deleting user: Camila +``` + +
+ +!!! tip + Everything to the *right* of the *command* are *CLI parameters* (*CLI arguments* and *CLI options*) for that command. + +!!! note "Technical Details" + Actually, it's everything to the right of that command, *before any subcommand*. + + It's possible to have groups of *subcommands*, it's like if one *command* also had *subcommands*. And then those *subcommands* could have their own *CLI parameters*, taking their own *CLI parameters*. + + You will see about them later in another section. diff --git a/docs/tutorial/commands/help.md b/docs/tutorial/commands/help.md new file mode 100644 index 0000000..e3e811b --- /dev/null +++ b/docs/tutorial/commands/help.md @@ -0,0 +1,120 @@ +The same as before, you can add help for the commands in the docstrings and the *CLI options*. + +And the `typer.Typer()` application receives a parameter `help` that you can pass with the main help text for your CLI program: + +```Python hl_lines="3 8 9 10 20 23 24 25 26 27 39 42 43 44 45 46 55 56 57" +{!./src/commands/help/tutorial001.py!} +``` + +Check it: + +
+ +```console +// Check the new help +$ python main.py --help + +Usage: main.py [OPTIONS] COMMAND [ARGS]... + + Awesome CLI user manager. + +Options: + --install-completion Install completion for the current shell. + --show-completion Show completion for the current shell, to copy it or customize the installation. + --help Show this message and exit. + +Commands: + create Create a new user with USERNAME. + delete Delete a user with USERNAME. + delete-all Delete ALL users in the database. + init Initialize the users database. + +// Now the commands have inline help šŸŽ‰ + +// Check the help for create +$ python main.py create --help + +Usage: main.py create [OPTIONS] USERNAME + + Create a new user with USERNAME. + +Options: + --help Show this message and exit. + +// Check the help for delete +$ python main.py delete --help + +Usage: main.py delete [OPTIONS] USERNAME + + Delete a user with USERNAME. + + If --force is not used, will ask for confirmation. + +Options: + --force / --no-force Force deletion without confirmation. [required] + --help Show this message and exit. + +// Check the help for delete-all +$ python main.py delete-all --help + +Usage: main.py delete-all [OPTIONS] + + Delete ALL users in the database. + + If --force is not used, will ask for confirmation. + +Options: + --force / --no-force Force deletion without confirmation. [required] + --help Show this message and exit. + +// Check the help for init +$ python main.py init --help + +Usage: main.py init [OPTIONS] + + Initialize the users database. + +Options: + --help Show this message and exit. +``` + +
+ +!!! tip + `typer.Typer()` receives several other parameters for other things, we'll see that later. + + You will also see how to use "Callbacks" later, and those include a way to add this same help message in a function docstring. + +## Overwrite command help + +You will probably be better adding the help text as a docstring to your functions, but if for some reason you wanted to overwrite it, you can use the `help` function argument passed to `@app.command()`: + +```Python hl_lines="6 14" +{!./src/commands/help/tutorial002.py!} +``` + +Check it: + +
+ +```console +// Check the help +$ python main.py --help + +// Notice it uses the help passed to @app.command() +Usage: main.py [OPTIONS] COMMAND [ARGS]... + +Options: + --install-completion Install completion for the current shell. + --show-completion Show completion for the current shell, to copy + it or customize the installation. + --help Show this message and exit. + +Commands: + create Create a new user with USERNAME. + delete Delete a user with USERNAME. + +// It uses "Create a new user with USERNAME." instead of "Some internal utility function to create." +``` + +
diff --git a/docs/tutorial/commands/index.md b/docs/tutorial/commands/index.md new file mode 100644 index 0000000..f06a5d2 --- /dev/null +++ b/docs/tutorial/commands/index.md @@ -0,0 +1,182 @@ +We have seen how to create a CLI program with possibly several *CLI Options* and *CLI Arguments*. + +But **Typer** allows you to create CLI programs with several commands (also known as subcommands). + +For example, the program `git` has several commands. + +One command of `git` is `git push`. And `git push` in turn takes its own *CLI arguments* and *CLI options*. + +For example: + +
+ +```console +// The push command with no parameters +$ git push + +---> 100% + +// The push command with one CLI option --set-upstream and 2 CLI arguments +$ git push --set-upstream origin master + +---> 100% +``` + +
+ +Another command of `git` is `git pull`, it also has some *CLI parameters*. + +It's like if the same big program `git` had several small programs inside. + +!!! tip + A command looks the same as a *CLI argument*, it's just some name without a preceding `--`. But commands have predefined name, and are used to group different sets of functionalities into the same CLI application. + +## Command or subcommand + +It's common to call a CLI program a "command". + +But when one of these programs have subcommands, those subcommands are also frequently called just "commands". + +Have that in mind so you don't get confused. + +Here I'll use **CLI application** or **program** to refer to the program you are building in Python with Typer, and **command** to refer to one of these "subcommands" of your program. + +## Explicit application + +Before creating CLI applications with multiple commands/subcommands we need to understand how to create an explicit `typer.Typer()` application. + +In the *CLI Options* and *CLI Argument* tutorials you have seen how to create a single function and then pass that function to `typer.run()`. + +For example: + +```Python hl_lines="9" +{!./src/first_steps/tutorial002.py!} +``` + +But that is actually a shortcut. Under the hood, **Typer** converts that to a CLI application with `typer.Typer()` and executes it. All that inside of `typer.run()`. + +There's also a more explicit way to achieve the same: + +```Python hl_lines="3 6 12" +{!./src/commands/index/tutorial001.py!} +``` + +When you use `typer.run()`, **Typer** is doing more or less the same as above, it will: + +* Create a new `typer.Typer()` "application". +* Create a new "`command`" with your function. +* Call the same "application" as if it was a function with "`app()`". + +!!! info "`@decorator` Info" + That `@something` syntax in Python is called a "decorator". + + You put it on top of a function. Like a pretty decorative hat (I guess that's where the term came from). + + A "decorator" takes the function below and does something with it. + + In our case, this decorator tells **Typer** that the function below is a "`command`". + +Both ways, with `typer.run()` and creating the explicit application, achieve the same. + +!!! tip + If your use case is solved with just `typer.run()`, that's fine, you don't have to create the explicit `app` and use `@app.command()`, etc. + + You might want to do that later when your app needs the extra features, but if it doesn't need them yet, that's fine. + +If you run the second example, with the explicit `app`, it works exactly the same: + +
+ +```console +// Without a CLI argument +$ python main.py + +Usage: main.py [OPTIONS] NAME +Try "main.py --help" for help. + +Error: Missing argument "NAME". + +// With the NAME CLI argument +$ python main.py Camila + +Hello Camila + +// Asking for help +$ python main.py --help + +Usage: main.py [OPTIONS] NAME + +Options: + --install-completion Install completion for the current shell. + --show-completion Show completion for the current shell, to copy it or customize the installation. + --help Show this message and exit. +``` + +
+ +## A CLI application with multiple commands + +Coming back to the CLI applications with multiple commands/subcommands, **Typer** allows creating CLI applications with multiple of them. + +Now that you know how to create an explicit `typer.Typer()` application and add one command, let's see how to add multiple commands. + +Let's say that we have a CLI application to manage users. + +We'll have a command to `create` users and another command to `delete` them. + +To begin, let's say it can only create and delete one single predefined user: + +```Python hl_lines="6 11" +{!./src/commands/index/tutorial002.py!} +``` + +Now we have a CLI application with 2 commands, `create` and `delete`: + +
+ +```console +// Check the help +$ python main.py --help + +Usage: main.py [OPTIONS] COMMAND [ARGS]... + +Options: + --install-completion Install completion for the current shell. + --show-completion Show completion for the current shell, to copy it or customize the installation. + --help Show this message and exit. + +Commands: + create + delete + +// Test them +$ python main.py create + +Creating user: Hiro Hamada + +$ python main.py delete + +Deleting user: Hiro Hamada + +// Now we have 2 commands! šŸŽ‰ +``` + +
+ +Notice that the help text now shows the 2 commands: `create` and `delete`. + +!!! tip + By default, the names of the commands are generated from the function name. + +## Decorator Technical Details + +When you use `@app.command()` the function under the decorator is registered in the **Typer** application and is then used later by the application. + +But Typer doesn't modify that function itself, the function is left as is. + +That means that if your function is simple enough that you could create it without using `typer.Option()` or `typer.Argument()`, you could use the same function for a **Typer** application and a **FastAPI** application putting both decorators on top, or similar tricks. + +!!! note "Click Technical Details" + This behavior is a design difference with Click. + + In Click, when you add a `@click.command()` decorator it actually modifies the function underneath and replaces it with an object. diff --git a/docs/tutorial/commands/name.md b/docs/tutorial/commands/name.md new file mode 100644 index 0000000..00011fa --- /dev/null +++ b/docs/tutorial/commands/name.md @@ -0,0 +1,46 @@ +By default, the command names are generated from the function name. + +So, if your function is something like: + +```Python +def create(username: str): + ... +``` + +Then the command name will be `create`. + +But if you already had a function called `create()` somewhere in your code, you would have to name your CLI function differently. + +And what if you wanted the command to still be named `create`? + +For this, you can set the name of the command in the first parameter for the `@app.command()` decorator: + +```Python hl_lines="6 11" +{!./src/commands/name/tutorial001.py!} +``` + +Now, even though the functions are named `cli_create_user()` and `cli_delete_user()`, the commands will still be named `create` and `delete`: + +
+ +```console +$ python main.py --help + +Usage: main.py [OPTIONS] COMMAND [ARGS]... + +Options: + --install-completion Install completion for the current shell. + --show-completion Show completion for the current shell, to copy it or customize the installation. + --help Show this message and exit. + +Commands: + create + delete + +// Test it +$ python main.py create Camila + +Creating user: Camila +``` + +
diff --git a/docs/tutorial/commands/one-or-multiple.md b/docs/tutorial/commands/one-or-multiple.md new file mode 100644 index 0000000..cab0da3 --- /dev/null +++ b/docs/tutorial/commands/one-or-multiple.md @@ -0,0 +1,91 @@ +You might have noticed that if you create a single command, as in the first example: + +```Python hl_lines="3 6 12" +{!./src/commands/tutorial001.py!} +``` + +**Typer** is smart enough to create a CLI application with that single function as the main CLI application, not as a command/subcommand: + +
+ +```console +// Without a CLI argument +$ python main.py + +Usage: main.py [OPTIONS] NAME +Try "main.py --help" for help. + +Error: Missing argument "NAME". + +// With the NAME CLI argument +$ python main.py Camila + +Hello Camila + +// Asking for help +$ python main.py + +Usage: main.py [OPTIONS] NAME + +Options: + --install-completion Install completion for the current shell. + --show-completion Show completion for the current shell, to copy it or customize the installation. + --help Show this message and exit. +``` + +
+ +!!! tip + Notice that it doesn't show a command `main`, even though the function name is `main`. + +But if you add multiple commands, **Typer** will create one *CLI command* for each one of them: + +```Python hl_lines="6 11" +{!./src/commands/tutorial002.py!} +``` + +Here we have 2 commands `create` and `delete`: + +
+ +```console +// Check the help +$ python main.py --help + +Usage: main.py [OPTIONS] COMMAND [ARGS]... + +Options: + --install-completion Install completion for the current shell. + --show-completion Show completion for the current shell, to copy it or customize the installation. + --help Show this message and exit. + +Commands: + create + delete + +// Test the commands +$ python main.py create + +Creating user: Hiro Hamada + +$ python main.py delete + +Deleting user: Hiro Hamada +``` + +
+ +!!! tip + This is **Typer**'s behavior by default, but you could still create a CLI application with one single command, like: + + ``` + $ python main.py main + ``` + + instead of just: + + ``` + $ python main.py + ``` + + You will learn more about this in the section about application Callbacks. diff --git a/docs/tutorial/commands/options.md b/docs/tutorial/commands/options.md new file mode 100644 index 0000000..40e4ca9 --- /dev/null +++ b/docs/tutorial/commands/options.md @@ -0,0 +1,96 @@ +Commands can also have their own *CLI options*. + +In fact, each command can have different *CLI arguments* and *CLI options*: + +```Python hl_lines="7 13 14 24 33" +{!./src/commands/options/tutorial001.py!} +``` + +Here we have multiple commands, with different *CLI parameters*: + +* `create`: + * `username`: a *CLI argument*. +* `delete`: + * `username`: a *CLI argument*. + * `--force`: a *CLI option*, if not provided, it's prompted. +* `delete-all`: + * `--force`: a *CLI option*, if not provided, it's prompted. +* `init`: + * Doesn't take any *CLI parameters*. + +
+ +```console +// Check the help +python main.py --help + +Usage: main.py [OPTIONS] COMMAND [ARGS]... + +Options: + --install-completion Install completion for the current shell. + --show-completion Show completion for the current shell, to copy it or customize the installation. + --help Show this message and exit. + +Commands: + create + delete + delete-all + info +``` + +
+ +!!! tip + Check the command `delete-all`, by default command names are generated from the function name, replacing `_` with `-`. + +Test it: + +
+ +```console +// Check the command create +$ python main.py create Camila + +Creating user: Camila + +// Now test the command delete +$ python main.py delete Camila + +# Are you sure you want to delete the user? [y/N]: $ y + +Deleting user: Camila + +$ python main.py delete Wade + +# Are you sure you want to delete the user? [y/N]: $ n + +Operation cancelled + +// And finally, the command delete-all +// Notice it doesn't have CLI arguments, only a CLI option + +$ python main.py delete-all + +# Are you sure you want to delete ALL users? [y/N]: $ y + +Deleting all users + +$ python main.py delete-all + +# Are you sure you want to delete ALL users? [y/N]: $ n + +Operation cancelled + +// And if you pass the --force CLI option, it doesn't need to confirm + +$ python main.py delete-all --force + +Deleting all users + +// And init that doesn't take any CLI parameter +$ python main.py init + +Initializing user database +``` + +
diff --git a/docs/tutorial/options.md b/docs/tutorial/options.md deleted file mode 100644 index a1dc05d..0000000 --- a/docs/tutorial/options.md +++ /dev/null @@ -1,263 +0,0 @@ -## *CLI options* with help - -In the *First Steps* section you saw how to add help for a CLI app/command by adding it to a function's docstring. - -Here's how that last example looked like: - -```Python -{!./src/first_steps/tutorial006.py!} -``` - -Now we'll add a *help* section to the *CLI options*: - -```Python hl_lines="6 7" -{!./src/options/tutorial001.py!} -``` - -We are replacing the default values we had before with `typer.Option()`. - -As we no longer have a default value there, the first parameter to `typer.Option()` serves the same purpose of defining that default value. - -So, if we had: - -```Python -lastname: str = "" -``` - -now we write: - -```Python -lastname: str = typer.Option("") -``` - -And both forms achieve the same: a *CLI option* with a default value of an empty string (`""`). - -And then we can pass the `help` keyword parameter: - -```Python -lastname: str = typer.Option("", help="this option does this and that") -``` - -to create the help for that *CLI option*. - -Copy that example from above to a file `main.py`. - -Test it: - -
- -```console -$ python main.py --help - -Usage: main.py [OPTIONS] NAME - - Say hi to NAME, optionally with a --lastname. - - If --formal is used, say hi very formally. - -Options: - --lastname TEXT Last name of person to greet. - --formal / --no-formal Say hi formally. - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. - --help Show this message and exit. - -// Now you have a help text for the --lastname and --formal CLI options šŸŽ‰ -``` - -
- -## Make a *CLI option* required - -We said before that *by default*: - -* *CLI options* are **optional** -* *CLI arguments* are **required** - -Well, that's how they work *by default*, and that's the convention in many CLI programs and systems. - -But if you really want, you can change that. - -To make a *CLI option* required, pass `...` to `typer.Option()`. - -!!! info - If you hadn't seen that `...` before: it is a a special single value, it is part of Python and is called "Ellipsis". - -That will tell **Typer** that it's still a *CLI option*, but it doesn't have a default value, and it's required. - -Let's make the `--lastname` a required *CLI option*. - -We'll also simplify the example to focus on the new parts: - -```Python hl_lines="4" -{!./src/options/tutorial002.py!} -``` - -!!! tip - You could still add `help` to `typer.Option()` as before, but we are omitting it here to simplify the example. - -And test it: - -
- -```console -// Pass the NAME CLI argument -$ python main.py Camila - -// We didn't pass the now required --lastname CLI option -Usage: main.py [OPTIONS] NAME -Try "main.py --help" for help. - -Error: Missing option "--lastname". - -// Now update it to pass the required --lastname CLI option -$ python main.py Camila --lastname GutiĆ©rrez - -Hello Camila GutiĆ©rrez - -// And if you check the help -$ python main.py --help - -Usage: main.py [OPTIONS] NAME - -Options: - --lastname TEXT [required] - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. - --help Show this message and exit. - -// It now tells you that --lastname is required šŸŽ‰ -``` - -
- -## Prompt for a *CLI option* - -It's also possible to, instead of just showing an error, ask for the missing value with `prompt=True`: - -```Python hl_lines="4" -{!./src/options/tutorial003.py!} -``` - -And then your program will ask the user for it in the terminal: - -
- -```console -// Call it with the NAME CLI argument -$ python main.py Camila - -// It asks for the missing CLI option --lastname -# Lastname: $ GutiƩrrez - -Hello Camila GutiƩrrez -``` - -
- -### Customize the prompt - -You can also set a custom prompt, passing the string that you want to use instead of just `True`: - -```Python hl_lines="6" -{!./src/options/tutorial004.py!} -``` - -And then your program will ask for it using with your custom prompt: - -
- -```console -// Call it with the NAME CLI argument -$ python main.py Camila - -// It uses the custom prompt -# Please tell me your last name: $ GutiƩrrez - -Hello Camila GutiƩrrez -``` - -
- -## Confirmation prompt - -In some cases you could want to prompt for something and then ask the user to confirm it by typing it twice. - -You can do it passing the parameter `confirmation_prompt=True`. - -Let's say it's a CLI app to delete a project: - -```Python hl_lines="4" -{!./src/options/tutorial005.py!} -``` - -And it will prompt the user for a value and then for the confirmation: - -
- -```console -$ python main.py - -// Your app will first prompt for the project name, and then for the confirmation -# Project name: $ Old Project -# Repeat for confirmation: $ Old Project - -Deleting project Old Project - -// If the user doesn't type the same, receives an error and a new prompt -$ python main.py - -# Project name: $ Old Project -# Repeat for confirmation: $ New Spice - -Error: the two entered values do not match - -# Project name: $ Old Project -# Repeat for confirmation: $ Old Project - -Deleting project Old Project - -// Now it works šŸŽ‰ -``` - -
- -## Show default in help - -You can tell Typer to show the default value in the help text with `show_default=True`: - -```Python hl_lines="4" -{!./src/options/tutorial006.py!} -``` - -And it will show up in the help text: - -
- -```console -$ python main.py - -Hello Wade Wilson - -// Show the help -$ python main.py --help - -Usage: main.py [OPTIONS] - -Options: - --fullname TEXT [default: Wade Wilson] - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. - --help Show this message and exit. -``` - -
- -!!! tip - Notice the `[default: Wade Wilson]` in the help text. - -## Other uses - -`typer.Option()` has several other users. For data validation, to enable other features, etc. - -But you will see about that later in the docs. diff --git a/docs/tutorial/options/help.md b/docs/tutorial/options/help.md new file mode 100644 index 0000000..688da78 --- /dev/null +++ b/docs/tutorial/options/help.md @@ -0,0 +1,100 @@ +In the *First Steps* section you saw how to add help for a CLI app/command by adding it to a function's docstring. + +Here's how that last example looked like: + +```Python +{!./src/first_steps/tutorial006.py!} +``` + +Now we'll add a *help* section to the *CLI options*: + +```Python hl_lines="6 7" +{!./src/options/help/tutorial001.py!} +``` + +We are replacing the default values we had before with `typer.Option()`. + +As we no longer have a default value there, the first parameter to `typer.Option()` serves the same purpose of defining that default value. + +So, if we had: + +```Python +lastname: str = "" +``` + +now we write: + +```Python +lastname: str = typer.Option("") +``` + +And both forms achieve the same: a *CLI option* with a default value of an empty string (`""`). + +And then we can pass the `help` keyword parameter: + +```Python +lastname: str = typer.Option("", help="this option does this and that") +``` + +to create the help for that *CLI option*. + +Copy that example from above to a file `main.py`. + +Test it: + +
+ +```console +$ python main.py --help + +Usage: main.py [OPTIONS] NAME + + Say hi to NAME, optionally with a --lastname. + + If --formal is used, say hi very formally. + +Options: + --lastname TEXT Last name of person to greet. + --formal / --no-formal Say hi formally. + --install-completion Install completion for the current shell. + --show-completion Show completion for the current shell, to copy it or customize the installation. + --help Show this message and exit. + +// Now you have a help text for the --lastname and --formal CLI options šŸŽ‰ +``` + +
+ +## Show default in help + +You can tell Typer to show the default value in the help text with `show_default=True`: + +```Python hl_lines="4" +{!./src/options/help/tutorial002.py!} +``` + +And it will show up in the help text: + +
+ +```console +$ python main.py + +Hello Wade Wilson + +// Show the help +$ python main.py --help + +Usage: main.py [OPTIONS] + +Options: + --fullname TEXT [default: Wade Wilson] + --install-completion Install completion for the current shell. + --show-completion Show completion for the current shell, to copy it or customize the installation. + --help Show this message and exit. +``` + +
+ +!!! tip + Notice the `[default: Wade Wilson]` in the help text. diff --git a/docs/tutorial/options/index.md b/docs/tutorial/options/index.md new file mode 100644 index 0000000..13c4505 --- /dev/null +++ b/docs/tutorial/options/index.md @@ -0,0 +1,3 @@ +In the next short sections we will see how to modify *CLI options* using `typer.Option()`. + +`typer.Option()` works very similarly to `typer.Argument()`, but has some extra features that we'll see next. diff --git a/docs/tutorial/options/name.md b/docs/tutorial/options/name.md new file mode 100644 index 0000000..3e61137 --- /dev/null +++ b/docs/tutorial/options/name.md @@ -0,0 +1,328 @@ +By default **Typer** will create a *CLI option* name from the function parameter. + +So, if you have a function with: + +```Python +def main(user_name: str = None): + pass +``` + +or + +```Python +def main(user_name: str = typer.Option(None)): + pass +``` + +**Typer** will create a *CLI option*: + +``` +--user-name +``` + +But you can customize it if you want to. + +Let's say the function parameter name is `user_name` as above, but you want the *CLI option* to be just `--name`. + +You can pass the *CLI option* name that you want to have in the next positional argument passed to `typer.Option()`: + +```Python hl_lines="4" +{!./src/options/name/tutorial001.py!} +``` + +Here you are passing the string `"--name"` as the second positional argument to `typer.Option()`. + +!!! info + "Positional" means that it's not a function argument with a keyword name. + + For example `show_default=True` is a keyword argument. "`show_default`" is the keyword. + + But in `"--name"` there's no `option_name="--name"` or something similar, it's just the string value `"--name"` that goes in `typer.Option()` after the `...` value passed in the first position. + + That's a "positional argument" in a function. + +Check it: + +
+ +```console +$ python main.py --help + +// Notice the --name instead of --user-name +Usage: main.py [OPTIONS] + +Options: + --name TEXT [required] + --install-completion Install completion for the current shell. + --show-completion Show completion for the current shell, to copy it or customize the installation. + --help Show this message and exit. + +// Try it +$ python --name Camila + +Hello Camila +``` + +
+ +## *CLI option* short names + +A short name is a *CLI option* name with a single dash (`-`) instead of 2 (`--`) and a single letter, like `-n` instead of `--name`. + +For example, the `ls` program has a *CLI option* named `--size`, and the same *CLI option* also has a short name `-s`: + +
+ +```console +// With the long name --size +$ ls ./myproject --size + +12 first-steps.md 4 intro.md + +// With the short name -s +$ ls ./myproject -s + +12 first-steps.md 4 intro.md + +// Both CLI option names do the same +``` + +
+ +### *CLI option* short names together + +Short names have another feature, when they have a single letter, as in `-s`, you can put several of these *CLI options* together, with a single dash. + +For example, the `ls` program has these 2 *CLI options* (among others): + +* `--size`: show the sizes of the listed files. +* `--human`: show a human-readable format, like `1MB` instead of just `1024`. + +And these 2 *CLI options* have short versions too: + +* `--size`: short version `-s`. +* `--human`: short version `-h`. + +So, you can put them together with `-sh` or `-hs`: + +
+ +```console +// Call ls with long CLI options +$ ls --size --human + +12K first-steps.md 4.0K intro.md + +// Now with short versions +$ ls -s -h + +12K first-steps.md 4.0K intro.md + +// And with short versions together +$ ls -sh + +12K first-steps.md 4.0K intro.md + +// Order in short versions doesn't matter +$ ls -hs + +12K first-steps.md 4.0K intro.md + +// They all work the same šŸŽ‰ +``` + +
+ +### *CLI option* short names with values + +When you use *CLI options* with short names, you can put them together if they are just boolean flags, like `--size` or `--human`. + +But if you have a *CLI option* `--file` with a short name `-f` that takes a value, if you put it with other short names for *CLI options*, you have to put it as the last letter, so that it can receive the value that comes right after. + +For example, let's say you are decompressing/extracting a file `myproject.tar.gz` with the program `tar`. + +You can pass these *CLI option* short names to `tar`: + +* `-x`: means "e`X`tract", to decompress and extract the contents. +* `-v`: means "`V`erbose", to print on the screen what it is doing, so you can know that it's decompressing each file and can entertain yourself while you wait. +* `-f`: means "`F`ile", this one requires a value, the compressed file to extract (in our example, this is `myproject.tar.gz`). + * So if you use all the short names together, this `-f` has to come last, to receive the value that comes next to it. + +For example: + +
+ +```console +$ tar -xvf myproject.tar.gz + +myproject/ +myproject/first-steps.md +myproject/intro.md + +// But if you put the -f before +$ tar -fxv myproject.tar.gz + +// You get an ugly error +tar: You must specify one of the blah, blah, error, error +``` + +
+ +### Defining *CLI option* short names + +In **Typer** you can also define *CLI option* short names the same way you can customize the long names. + +`typer.Option()` receives as a first function argument the default value, e.g. `None`, and all the next *positional* values are to define the *CLI option* name(s). + +!!! tip + Remember the *positional* function arguments are those that don't have a keyword. + + All the other function arguments/parameters you pass to `typer.Option()` like `prompt=True` and `help="This option blah, blah"` require the keyword. + +You can overwrite the *CLI option* name to use as in the previous example, but you can also declare extra alternatives, including short names. + +For example, extending the previous example, let's add a *CLI option* short name `-n`: + +```Python hl_lines="4" +{!./src/options/name/tutorial002.py!} +``` + +Here we are overwriting the *CLI option* name that by default would be `--user-name`, and we are defining it to be `--name`. And we are also declaring a *CLI option* short name of `-n`. + +Check it: + +
+ +```console +// Check the help +$ python main.py --help + +// Notice the two CLI option names -n and --name +Usage: main.py [OPTIONS] + +Options: + -n, --name TEXT [required] + --install-completion Install completion for the current shell. + --show-completion Show completion for the current shell, to copy it or customize the installation. + --help Show this message and exit. + +// Try the short version +$ python main.py -n Camila + +Hello Camila +``` + +
+ +### *CLI option* only short name + +If you only declare a short name like `-n` then that will be the only *CLI option* name. And neither `--name` nor `--user-name` will be available. + +```Python hl_lines="4" +{!./src/options/name/tutorial003.py!} +``` + +Check it: + +
+ +```console +$ python main.py --help + +// Notice there's no --name nor --user-name, only -n +Usage: main.py [OPTIONS] + +Options: + -n TEXT [required] + --install-completion Install completion for the current shell. + --show-completion Show completion for the current shell, to copy it or customize the installation. + --help Show this message and exit. + +// Try it +$ python main.py -n Camila + +Hello Camila +``` + +
+ +### *CLI option* short name and default + +Continuing with the example above, as **Typer** allows you to declare a *CLI option* as having only a short name, if you want to have the default long name plus a short name, you have to declare both explicitly: + +```Python hl_lines="4" +{!./src/options/name/tutorial004.py!} +``` + +Check it: + +
+ +```console +$ python main.py --help + +// Notice that we have the long version --user-name back +// and we also have the short version -n +Usage: main.py [OPTIONS] + +Options: + -n, --user-name TEXT [required] + --install-completion Install completion for the current shell. + --show-completion Show completion for the current shell, to copy it or customize the installation. + --help Show this message and exit. + +// Try it +$ python main.py --user-name Camila + +Hello Camila + +// And try the short version +$ python main.py -n Camila +``` + +
+ +### *CLI option* short names together + +You can create multiple short names and use them together. + +You don't have to do anything special for it to work (apart from declaring those short versions): + +```Python hl_lines="5 6" +{!./src/options/name/tutorial005.py!} +``` + +!!! tip + Notice that, again, we are declaring the long and short version of the *CLI option* names. + +Check it: + +
+ +```console +$ python main.py --help + +// We now have short versions -n and -f +// And also long versions --name and --formal +Usage: main.py [OPTIONS] + +Options: + -n, --name TEXT [required] + -f, --formal + --install-completion Install completion for the current shell. + --show-completion Show completion for the current shell, to copy it or customize the installation. + --help Show this message and exit. + +// Try the short versions +$ python main.py -n Camila -f + +Good day Ms. Camila. + +// And try the 2 short versions together +// See how -n has to go last, to be able to get the value +$ python main.py -fn Camila + +Good day Ms. Camila. +``` + +
diff --git a/docs/tutorial/options/prompt.md b/docs/tutorial/options/prompt.md new file mode 100644 index 0000000..70a1c43 --- /dev/null +++ b/docs/tutorial/options/prompt.md @@ -0,0 +1,88 @@ +It's also possible to, instead of just showing an error, ask for the missing value with `prompt=True`: + +```Python hl_lines="4" +{!./src/options/prompt/tutorial001.py!} +``` + +And then your program will ask the user for it in the terminal: + +
+ +```console +// Call it with the NAME CLI argument +$ python main.py Camila + +// It asks for the missing CLI option --lastname +# Lastname: $ GutiƩrrez + +Hello Camila GutiƩrrez +``` + +
+ +## Customize the prompt + +You can also set a custom prompt, passing the string that you want to use instead of just `True`: + +```Python hl_lines="6" +{!./src/options/prompt/tutorial002.py!} +``` + +And then your program will ask for it using with your custom prompt: + +
+ +```console +// Call it with the NAME CLI argument +$ python main.py Camila + +// It uses the custom prompt +# Please tell me your last name: $ GutiƩrrez + +Hello Camila GutiƩrrez +``` + +
+ +## Confirmation prompt + +In some cases you could want to prompt for something and then ask the user to confirm it by typing it twice. + +You can do it passing the parameter `confirmation_prompt=True`. + +Let's say it's a CLI app to delete a project: + +```Python hl_lines="4" +{!./src/options/prompt/tutorial003.py!} +``` + +And it will prompt the user for a value and then for the confirmation: + +
+ +```console +$ python main.py + +// Your app will first prompt for the project name, and then for the confirmation +# Project name: $ Old Project +# Repeat for confirmation: $ Old Project + +Deleting project Old Project + +// If the user doesn't type the same, receives an error and a new prompt +$ python main.py + +# Project name: $ Old Project +# Repeat for confirmation: $ New Spice + +Error: the two entered values do not match + +# Project name: $ Old Project +# Repeat for confirmation: $ Old Project + +Deleting project Old Project + +// Now it works šŸŽ‰ +``` + +
diff --git a/docs/tutorial/options/required.md b/docs/tutorial/options/required.md new file mode 100644 index 0000000..39d52ab --- /dev/null +++ b/docs/tutorial/options/required.md @@ -0,0 +1,56 @@ +We said before that *by default*: + +* *CLI options* are **optional** +* *CLI arguments* are **required** + +Well, that's how they work *by default*, and that's the convention in many CLI programs and systems. + +But if you really want, you can change that. + +To make a *CLI option* required, pass `...` to `typer.Option()`. + +!!! info + If you hadn't seen that `...` before: it is a a special single value, it is part of Python and is called "Ellipsis". + +That will tell **Typer** that it's still a *CLI option*, but it doesn't have a default value, and it's required. + +Let's make `--lastname` a required *CLI option*: + +```Python hl_lines="4" +{!./src/options/required/tutorial001.py!} +``` + +And test it: + +
+ +```console +// Pass the NAME CLI argument +$ python main.py Camila + +// We didn't pass the now required --lastname CLI option +Usage: main.py [OPTIONS] NAME +Try "main.py --help" for help. + +Error: Missing option "--lastname". + +// Now update it to pass the required --lastname CLI option +$ python main.py Camila --lastname GutiĆ©rrez + +Hello Camila GutiĆ©rrez + +// And if you check the help +$ python main.py --help + +Usage: main.py [OPTIONS] NAME + +Options: + --lastname TEXT [required] + --install-completion Install completion for the current shell. + --show-completion Show completion for the current shell, to copy it or customize the installation. + --help Show this message and exit. + +// It now tells you that --lastname is required šŸŽ‰ +``` + +
diff --git a/mkdocs.yml b/mkdocs.yml index 12a20e2..fe4b480 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -23,9 +23,20 @@ nav: - Tutorial - User Guide: - Tutorial - User Guide - Intro: 'tutorial/index.md' - First Steps: 'tutorial/first-steps.md' - - CLI Options: 'tutorial/options.md' - CLI Arguments: 'tutorial/arguments.md' - - Commands: 'tutorial/commands.md' + - CLI Options: + - CLI Options Intro: 'tutorial/options/index.md' + - CLI Options with Help: 'tutorial/options/help.md' + - Required CLI Options: 'tutorial/options/required.md' + - CLI Option Prompt: 'tutorial/options/prompt.md' + - CLI Option Name: 'tutorial/options/name.md' + - Commands: + - Commands Intro: 'tutorial/commands/index.md' + - Command CLI Arguments: 'tutorial/commands/arguments.md' + - Command CLI Options: 'tutorial/commands/options.md' + - Command Help: 'tutorial/commands/help.md' + - Custom Command Name: 'tutorial/commands/name.md' + - One or Multiple Commands: 'tutorial/commands/one-or-multiple.md' - CLI Parameter Types: - CLI Parameter Types Intro: 'tutorial/parameter-types/index.md' - Number: 'tutorial/parameter-types/number.md' diff --git a/tests/test_tutorial/test_commands/test_arguments/__init__.py b/tests/test_tutorial/test_commands/test_arguments/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_tutorial/test_commands/test_tutorial003.py b/tests/test_tutorial/test_commands/test_arguments/test_tutorial001.py similarity index 95% rename from tests/test_tutorial/test_commands/test_tutorial003.py rename to tests/test_tutorial/test_commands/test_arguments/test_tutorial001.py index 3a5b379..cced821 100644 --- a/tests/test_tutorial/test_commands/test_tutorial003.py +++ b/tests/test_tutorial/test_commands/test_arguments/test_tutorial001.py @@ -1,5 +1,5 @@ import subprocess -from commands import tutorial003 as mod +from commands.arguments import tutorial001 as mod from typer.testing import CliRunner diff --git a/tests/test_tutorial/test_commands/test_help/__init__.py b/tests/test_tutorial/test_commands/test_help/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_tutorial/test_commands/test_tutorial005.py b/tests/test_tutorial/test_commands/test_help/test_tutorial001.py similarity index 98% rename from tests/test_tutorial/test_commands/test_tutorial005.py rename to tests/test_tutorial/test_commands/test_help/test_tutorial001.py index 8771766..eac6950 100644 --- a/tests/test_tutorial/test_commands/test_tutorial005.py +++ b/tests/test_tutorial/test_commands/test_help/test_tutorial001.py @@ -1,5 +1,5 @@ import subprocess -from commands import tutorial005 as mod +from commands.help import tutorial001 as mod from typer.testing import CliRunner diff --git a/tests/test_tutorial/test_commands/test_help/test_tutorial002.py b/tests/test_tutorial/test_commands/test_help/test_tutorial002.py new file mode 100644 index 0000000..1413247 --- /dev/null +++ b/tests/test_tutorial/test_commands/test_help/test_tutorial002.py @@ -0,0 +1,55 @@ +import subprocess +from commands.help import tutorial002 as mod + +from typer.testing import CliRunner + +app = mod.app + +runner = CliRunner() + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "create" in result.output + assert "Create a new user with USERNAME." in result.output + assert "delete" in result.output + assert "Delete a user with USERNAME." in result.output + assert "Some internal utility function to create." not in result.output + assert "Some internal utility function to delete." not in result.output + + +def test_help_create(): + result = runner.invoke(app, ["create", "--help"]) + assert result.exit_code == 0 + assert "Create a new user with USERNAME." in result.output + assert "Some internal utility function to create." not in result.output + + +def test_help_create(): + result = runner.invoke(app, ["delete", "--help"]) + assert result.exit_code == 0 + assert "Delete a user with USERNAME." in result.output + assert "Some internal utility function to delete." not in result.output + + +def test_create(): + result = runner.invoke(app, ["create", "Camila"]) + assert result.exit_code == 0 + assert "Creating user: Camila" in result.output + + +def test_delete(): + result = runner.invoke(app, ["delete", "Camila"]) + assert result.exit_code == 0 + assert "Deleting user: Camila" in result.output + + +def test_script(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_commands/test_index/__init__.py b/tests/test_tutorial/test_commands/test_index/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_tutorial/test_commands/test_tutorial001.py b/tests/test_tutorial/test_commands/test_index/test_tutorial001.py similarity index 93% rename from tests/test_tutorial/test_commands/test_tutorial001.py rename to tests/test_tutorial/test_commands/test_index/test_tutorial001.py index 33135f7..9bf27ce 100644 --- a/tests/test_tutorial/test_commands/test_tutorial001.py +++ b/tests/test_tutorial/test_commands/test_index/test_tutorial001.py @@ -1,5 +1,5 @@ import subprocess -from commands import tutorial001 as mod +from commands.index import tutorial001 as mod from typer.testing import CliRunner diff --git a/tests/test_tutorial/test_commands/test_tutorial002.py b/tests/test_tutorial/test_commands/test_index/test_tutorial002.py similarity index 95% rename from tests/test_tutorial/test_commands/test_tutorial002.py rename to tests/test_tutorial/test_commands/test_index/test_tutorial002.py index 0a5fc31..c87e404 100644 --- a/tests/test_tutorial/test_commands/test_tutorial002.py +++ b/tests/test_tutorial/test_commands/test_index/test_tutorial002.py @@ -1,5 +1,5 @@ import subprocess -from commands import tutorial002 as mod +from commands.index import tutorial002 as mod from typer.testing import CliRunner diff --git a/tests/test_tutorial/test_commands/test_name/__init__.py b/tests/test_tutorial/test_commands/test_name/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_tutorial/test_commands/test_tutorial006.py b/tests/test_tutorial/test_commands/test_name/test_tutorial001.py similarity index 95% rename from tests/test_tutorial/test_commands/test_tutorial006.py rename to tests/test_tutorial/test_commands/test_name/test_tutorial001.py index 3630db1..9dda088 100644 --- a/tests/test_tutorial/test_commands/test_tutorial006.py +++ b/tests/test_tutorial/test_commands/test_name/test_tutorial001.py @@ -1,5 +1,5 @@ import subprocess -from commands import tutorial006 as mod +from commands.name import tutorial001 as mod from typer.testing import CliRunner diff --git a/tests/test_tutorial/test_commands/test_options/__init__.py b/tests/test_tutorial/test_commands/test_options/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_tutorial/test_commands/test_tutorial004.py b/tests/test_tutorial/test_commands/test_options/test_tutorial001.py similarity index 97% rename from tests/test_tutorial/test_commands/test_tutorial004.py rename to tests/test_tutorial/test_commands/test_options/test_tutorial001.py index a2e26c7..573725b 100644 --- a/tests/test_tutorial/test_commands/test_tutorial004.py +++ b/tests/test_tutorial/test_commands/test_options/test_tutorial001.py @@ -1,5 +1,5 @@ import subprocess -from commands import tutorial004 as mod +from commands.options import tutorial001 as mod from typer.testing import CliRunner diff --git a/tests/test_tutorial/test_options/test_help/__init__.py b/tests/test_tutorial/test_options/test_help/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_tutorial/test_options/test_tutorial001.py b/tests/test_tutorial/test_options/test_help/test_tutorial001.py similarity index 96% rename from tests/test_tutorial/test_options/test_tutorial001.py rename to tests/test_tutorial/test_options/test_help/test_tutorial001.py index 835ed58..28b77e2 100644 --- a/tests/test_tutorial/test_options/test_tutorial001.py +++ b/tests/test_tutorial/test_options/test_help/test_tutorial001.py @@ -3,7 +3,7 @@ import subprocess import typer from typer.testing import CliRunner -from options import tutorial001 as mod +from options.help import tutorial001 as mod runner = CliRunner() diff --git a/tests/test_tutorial/test_options/test_tutorial006.py b/tests/test_tutorial/test_options/test_help/test_tutorial002.py similarity index 94% rename from tests/test_tutorial/test_options/test_tutorial006.py rename to tests/test_tutorial/test_options/test_help/test_tutorial002.py index df08073..1a9aa44 100644 --- a/tests/test_tutorial/test_options/test_tutorial006.py +++ b/tests/test_tutorial/test_options/test_help/test_tutorial002.py @@ -3,7 +3,7 @@ import subprocess import typer from typer.testing import CliRunner -from options import tutorial006 as mod +from options.help import tutorial002 as mod runner = CliRunner() diff --git a/tests/test_tutorial/test_options/test_name/__init__.py b/tests/test_tutorial/test_options/test_name/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_tutorial/test_options/test_name/test_tutorial001.py b/tests/test_tutorial/test_options/test_name/test_tutorial001.py new file mode 100644 index 0000000..0095441 --- /dev/null +++ b/tests/test_tutorial/test_options/test_name/test_tutorial001.py @@ -0,0 +1,34 @@ +import subprocess + +import typer +from typer.testing import CliRunner + +from options.name import tutorial001 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_option_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "--name TEXT" in result.output + assert "--user-name" not in result.output + + +def test_call(): + result = runner.invoke(app, ["--name", "Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" in result.output + + +def test_script(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_options/test_name/test_tutorial002.py b/tests/test_tutorial/test_options/test_name/test_tutorial002.py new file mode 100644 index 0000000..7e3ca59 --- /dev/null +++ b/tests/test_tutorial/test_options/test_name/test_tutorial002.py @@ -0,0 +1,40 @@ +import subprocess + +import typer +from typer.testing import CliRunner + +from options.name import tutorial002 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_option_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "-n, --name TEXT" in result.output + assert "--user-name" not in result.output + + +def test_call(): + result = runner.invoke(app, ["-n", "Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" in result.output + + +def test_call_long(): + result = runner.invoke(app, ["--name", "Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" in result.output + + +def test_script(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_options/test_name/test_tutorial003.py b/tests/test_tutorial/test_options/test_name/test_tutorial003.py new file mode 100644 index 0000000..647801e --- /dev/null +++ b/tests/test_tutorial/test_options/test_name/test_tutorial003.py @@ -0,0 +1,35 @@ +import subprocess + +import typer +from typer.testing import CliRunner + +from options.name import tutorial003 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_option_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "-n TEXT" in result.output + assert "--user-name" not in result.output + assert "--name" not in result.output + + +def test_call(): + result = runner.invoke(app, ["-n", "Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" in result.output + + +def test_script(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_options/test_name/test_tutorial004.py b/tests/test_tutorial/test_options/test_name/test_tutorial004.py new file mode 100644 index 0000000..a4c4602 --- /dev/null +++ b/tests/test_tutorial/test_options/test_name/test_tutorial004.py @@ -0,0 +1,40 @@ +import subprocess + +import typer +from typer.testing import CliRunner + +from options.name import tutorial004 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_option_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "-n, --user-name TEXT" in result.output + assert "--name" not in result.output + + +def test_call(): + result = runner.invoke(app, ["-n", "Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" in result.output + + +def test_call_long(): + result = runner.invoke(app, ["--user-name", "Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" in result.output + + +def test_script(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_options/test_name/test_tutorial005.py b/tests/test_tutorial/test_options/test_name/test_tutorial005.py new file mode 100644 index 0000000..fd30298 --- /dev/null +++ b/tests/test_tutorial/test_options/test_name/test_tutorial005.py @@ -0,0 +1,51 @@ +import subprocess + +import typer +from typer.testing import CliRunner + +from options.name import tutorial005 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_option_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "-n, --name TEXT" in result.output + assert "-f, --formal" in result.output + + +def test_call(): + result = runner.invoke(app, ["-n", "Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" in result.output + + +def test_call_formal(): + result = runner.invoke(app, ["-n", "Camila", "-f"]) + assert result.exit_code == 0 + assert "Good day Ms. Camila." in result.output + + +def test_call_formal_condensed(): + result = runner.invoke(app, ["-fn", "Camila"]) + assert result.exit_code == 0 + assert "Good day Ms. Camila." in result.output + + +def test_call_condensed_wrong_order(): + result = runner.invoke(app, ["-nf", "Camila"]) + assert result.exit_code != 0 + + +def test_script(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_options/test_prompt/__init__.py b/tests/test_tutorial/test_options/test_prompt/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_tutorial/test_options/test_tutorial003.py b/tests/test_tutorial/test_options/test_prompt/test_tutorial001.py similarity index 95% rename from tests/test_tutorial/test_options/test_tutorial003.py rename to tests/test_tutorial/test_options/test_prompt/test_tutorial001.py index 129e5d0..96a6adb 100644 --- a/tests/test_tutorial/test_options/test_tutorial003.py +++ b/tests/test_tutorial/test_options/test_prompt/test_tutorial001.py @@ -3,7 +3,7 @@ import subprocess import typer from typer.testing import CliRunner -from options import tutorial003 as mod +from options.prompt import tutorial001 as mod runner = CliRunner() diff --git a/tests/test_tutorial/test_options/test_tutorial004.py b/tests/test_tutorial/test_options/test_prompt/test_tutorial002.py similarity index 95% rename from tests/test_tutorial/test_options/test_tutorial004.py rename to tests/test_tutorial/test_options/test_prompt/test_tutorial002.py index 7946248..95fe480 100644 --- a/tests/test_tutorial/test_options/test_tutorial004.py +++ b/tests/test_tutorial/test_options/test_prompt/test_tutorial002.py @@ -3,7 +3,7 @@ import subprocess import typer from typer.testing import CliRunner -from options import tutorial004 as mod +from options.prompt import tutorial002 as mod runner = CliRunner() diff --git a/tests/test_tutorial/test_options/test_tutorial005.py b/tests/test_tutorial/test_options/test_prompt/test_tutorial003.py similarity index 96% rename from tests/test_tutorial/test_options/test_tutorial005.py rename to tests/test_tutorial/test_options/test_prompt/test_tutorial003.py index 3b4557a..076b4ee 100644 --- a/tests/test_tutorial/test_options/test_tutorial005.py +++ b/tests/test_tutorial/test_options/test_prompt/test_tutorial003.py @@ -3,7 +3,7 @@ import subprocess import typer from typer.testing import CliRunner -from options import tutorial005 as mod +from options.prompt import tutorial003 as mod runner = CliRunner() diff --git a/tests/test_tutorial/test_options/test_required/__init__.py b/tests/test_tutorial/test_options/test_required/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_tutorial/test_options/test_tutorial002.py b/tests/test_tutorial/test_options/test_required/test_tutorial002.py similarity index 94% rename from tests/test_tutorial/test_options/test_tutorial002.py rename to tests/test_tutorial/test_options/test_required/test_tutorial002.py index 77c880a..b9ed2e9 100644 --- a/tests/test_tutorial/test_options/test_tutorial002.py +++ b/tests/test_tutorial/test_options/test_required/test_tutorial002.py @@ -3,7 +3,7 @@ import subprocess import typer from typer.testing import CliRunner -from options import tutorial002 as mod +from options.required import tutorial001 as mod runner = CliRunner() diff --git a/typer/params.py b/typer/params.py index 820a0c5..3ee37de 100644 --- a/typer/params.py +++ b/typer/params.py @@ -106,7 +106,7 @@ def Option( def Argument( # Parameter default: Optional[Any], - *param_decls: str, + *, callback: Optional[Callable[[click.Context, click.Parameter, str], Any]] = None, metavar: Optional[str] = None, expose_value: bool = True, @@ -142,7 +142,9 @@ def Argument( return ArgumentInfo( # Parameter default=default, - param_decls=param_decls, + # Arguments can only have one param declaration + # it will be generated from the param name + param_decls=None, callback=callback, metavar=metavar, expose_value=expose_value,