diff --git a/docs/src/printing/tutorial001.py b/docs/src/printing/tutorial001.py new file mode 100644 index 0000000..a9d41c9 --- /dev/null +++ b/docs/src/printing/tutorial001.py @@ -0,0 +1,15 @@ +import typer + + +def main(good: bool = True): + message_start = "everything is " + if good: + ending = typer.style("good", fg=typer.colors.GREEN, bold=True) + else: + ending = typer.style("bad", fg=typer.colors.WHITE, bg=typer.colors.RED) + message = message_start + ending + typer.echo(message) + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs/src/printing/tutorial002.py b/docs/src/printing/tutorial002.py new file mode 100644 index 0000000..6400555 --- /dev/null +++ b/docs/src/printing/tutorial002.py @@ -0,0 +1,9 @@ +import typer + + +def main(name: str): + typer.secho(f"Welcome here {name}", fg=typer.colors.MAGENTA) + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs/src/terminating/tutorial001.py b/docs/src/terminating/tutorial001.py new file mode 100644 index 0000000..c77a09c --- /dev/null +++ b/docs/src/terminating/tutorial001.py @@ -0,0 +1,25 @@ +import typer + +existing_usernames = ["rick", "morty"] + + +def maybe_create_user(username: str): + if username in existing_usernames: + typer.echo("The user already exists") + raise typer.Exit() + else: + typer.echo(f"User created: {username}") + + +def send_new_user_notification(username: str): + # Somehow send a notification here for the new user, maybe an email + typer.echo(f"Notification sent for new user: {username}") + + +def main(username: str): + maybe_create_user(username=username) + send_new_user_notification(username=username) + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs/src/terminating/tutorial002.py b/docs/src/terminating/tutorial002.py new file mode 100644 index 0000000..f7af101 --- /dev/null +++ b/docs/src/terminating/tutorial002.py @@ -0,0 +1,12 @@ +import typer + + +def main(username: str): + if username == "root": + typer.echo("The root user is reserved") + raise typer.Exit(code=1) + typer.echo(f"New user created: {username}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs/src/terminating/tutorial003.py b/docs/src/terminating/tutorial003.py new file mode 100644 index 0000000..56e1406 --- /dev/null +++ b/docs/src/terminating/tutorial003.py @@ -0,0 +1,12 @@ +import typer + + +def main(username: str): + if username == "root": + typer.echo("The root user is reserved") + raise typer.Abort() + typer.echo(f"New user created: {username}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs/tutorial/first-steps.md b/docs/tutorial/first-steps.md index fedad69..42509e3 100644 --- a/docs/tutorial/first-steps.md +++ b/docs/tutorial/first-steps.md @@ -6,6 +6,9 @@ The simplest Typer file could look like this: {!./src/first_steps/tutorial001.py!} ``` +!!! tip + You will learn more about `typer.echo()` later in the docs. + Copy that to a file `main.py`. Test it: diff --git a/docs/tutorial/printing.md b/docs/tutorial/printing.md new file mode 100644 index 0000000..b8247e0 --- /dev/null +++ b/docs/tutorial/printing.md @@ -0,0 +1,83 @@ +You can use `typer.echo()` to print to the screen: + +```Python hl_lines="5" +{!./src/first_steps/tutorial001.py!} +``` + +The reason to use `typer.echo()` instead of just `print()` is that it applies some error corrections in case the terminal is misconfigured, and it will properly output color if it's supported. + +!!! info + `typer.echo()` comes directly from Click, you can read more about it in Click's docs. + +Check it: + +
+ +```console +$ python main.py + +Hello World +``` + +
+ +## Color + +!!! info + For colors to work correctly on Windows you need to also install `colorama`. + + You don't need to call `colorama.init()`. Typer (actually Click) will handle it underneath. + +!!! note "Technical Details" + The way color works in terminals is by using some codes (ASCII codes) as part of the text. + + So, a colored text is still just a `str`. + +You can create colored strings to output to the terminal with `typer.style()`, that gives you `str`s that you can then pass to `typer.echo()`: + +```Python hl_lines="7 9" +{!./src/printing/tutorial001.py!} +``` + +!!! tip + The parameters `fg` and `bg` receive strings with the color names. You could simply pass `fg="green"` and `bg="red"`. + + But **Typer** provides them all as variables like `typer.colors.GREEN` just so you can use autocompletion while selecting them. + +Check it: + +
+python main.py +everything is good +python main.py --no-good +everything is bad +
+ +You can pass these function arguments to `typer.style()`: + +* `fg`: the foreground color. +* `bg`: the background color. +* `bold`: enable or disable bold mode. +* `dim`: enable or disable dim mode. This is badly supported. +* `underline`: enable or disable underline. +* `blink`: enable or disable blinking. +* `reverse`: enable or disable inverse rendering (foreground becomes background and the other way round). +* `reset`: by default a reset-all code is added at the end of the string which means that styles do not carry over. This can be disabled to compose styles. + +!!! info + You can read more about it in Click's docs about `style()` + +## `typer.secho()` - style and print + +There's a shorter form to style and print at the same time with `typer.secho()` it's like `typer.echo()` but also adds style like `typer.style()`: + +```Python hl_lines="5" +{!./src/printing/tutorial002.py!} +``` + +Check it: + +
+python main.py Camila +Welcome here Camila +
diff --git a/docs/tutorial/terminating.md b/docs/tutorial/terminating.md new file mode 100644 index 0000000..49f8cc6 --- /dev/null +++ b/docs/tutorial/terminating.md @@ -0,0 +1,118 @@ +There are some cases where you might want to terminate a command at some point, and stop all subsequent execution. + +It could be that your code determined that the program completed successfully, or it could be an operation aborted. + +## `Exit` a CLI program + +You can normally just let the code of your CLI program finish its execution, but in some scenarios, you might want to terminate at some point in the middle of it. And prevent any subsequent code to run. + +This doesn't have to mean that there's an error, just that nothing else needs to be executed. + +In that case, you can raise a `typer.Exit()` exception: + +```Python hl_lines="9" +{!./src/terminating/tutorial001.py!} +``` + +There are several things to see in this example. + +* The CLI program is the function `main()`, not the others. This is the one that takes a *CLI argument*. +* The function `maybe_create_user()` can terminate the program by raising `typer.Exit()`. +* If the program is terminated by `maybe_create_user()` then `send_new_user_notification()` will never execute inside of `main()`. + +Check it: + +
+ +```console +$ python main.py Camila + +User created: Camila +Notification sent for new user: Camila + +// Try with an existing user +$ python main.py rick + +The user already exists + +// Notice that the notification code was never run, the second message is not printed +``` + +
+ +!!! tip + Even though you are rasing an exception, it doesn't necessarily mean there's an error. + + This is done with an exception because it works as an "error" and stops all execution. + + But then **Typer** (actually Click) catches it and just terminates the program normally. + +## Exit with an error + +`typer.Exit()` takes an optional `code` parameter. By default, `code` is `0`, meaning there was no error. + +You can pass a `code` with a number other than `0` to tell the terminal that there was an error in the execution of the program: + +```Python hl_lines="7" +{!./src/terminating/tutorial002.py!} +``` + +Check it: + +
+ +```console +$ python main.py Camila + +New user created: Camila + +// Print the result code of the last program executed +$ echo $? + +0 + +// Now make it exit with an error +$ python main.py root + +The root user is reserved + +// Print the result code of the last program executed +$ echo $? + +1 + +// 1 means there was an error, 0 means no errors. +``` + +
+ +!!! tip + The error code might be used by other programs (for example a Bash script) that execute with your CLI program. + +## Abort + +There's a special exception that you can use to "abort" a program. + +It works more or less the same as `typer.Exit()` but will print `"Aborted!"` to the screen and can be useful in certain cases later to make it explicit that the execution was aborted: + +```Python hl_lines="7" +{!./src/terminating/tutorial003.py!} +``` + +Check it: + +
+ +```console +$ python main.py Camila + +New user created: Camila + +// Now make it exit with an error +$ python main.py root + +The root user is reserved +Aborted! +``` + +
diff --git a/mkdocs.yml b/mkdocs.yml index fe4b480..3b9f028 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -23,6 +23,8 @@ nav: - Tutorial - User Guide: - Tutorial - User Guide - Intro: 'tutorial/index.md' - First Steps: 'tutorial/first-steps.md' + - Printing and Colors: 'tutorial/printing.md' + - Terminating: 'tutorial/terminating.md' - CLI Arguments: 'tutorial/arguments.md' - CLI Options: - CLI Options Intro: 'tutorial/options/index.md' diff --git a/tests/test_tutorial/test_terminating/__init__.py b/tests/test_tutorial/test_terminating/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_tutorial/test_terminating/test_tutorial001.py b/tests/test_tutorial/test_terminating/test_tutorial001.py new file mode 100644 index 0000000..bfc7e6a --- /dev/null +++ b/tests/test_tutorial/test_terminating/test_tutorial001.py @@ -0,0 +1,35 @@ +import subprocess + +import typer +from typer.testing import CliRunner + +from terminating import tutorial001 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_cli(): + result = runner.invoke(app, ["Camila"]) + assert result.exit_code == 0 + assert "User created: Camila" in result.output + assert "Notification sent for new user: Camila" in result.output + + +def test_existing(): + result = runner.invoke(app, ["rick"]) + assert result.exit_code == 0 + assert "The user already exists" in result.output + assert "Notification sent for new user" not 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_terminating/test_tutorial002.py b/tests/test_tutorial/test_terminating/test_tutorial002.py new file mode 100644 index 0000000..b12244b --- /dev/null +++ b/tests/test_tutorial/test_terminating/test_tutorial002.py @@ -0,0 +1,33 @@ +import subprocess + +import typer +from typer.testing import CliRunner + +from terminating import tutorial002 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_cli(): + result = runner.invoke(app, ["Camila"]) + assert result.exit_code == 0 + assert "New user created: Camila" in result.output + + +def test_root(): + result = runner.invoke(app, ["root"]) + assert result.exit_code == 1 + assert "The root user is reserved" 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_terminating/test_tutorial003.py b/tests/test_tutorial/test_terminating/test_tutorial003.py new file mode 100644 index 0000000..a19a614 --- /dev/null +++ b/tests/test_tutorial/test_terminating/test_tutorial003.py @@ -0,0 +1,34 @@ +import subprocess + +import typer +from typer.testing import CliRunner + +from terminating import tutorial003 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_cli(): + result = runner.invoke(app, ["Camila"]) + assert result.exit_code == 0 + assert "New user created: Camila" in result.output + + +def test_root(): + result = runner.invoke(app, ["root"]) + assert result.exit_code == 1 + assert "The root user is reserved" in result.output + assert "Aborted!" 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/typer/__init__.py b/typer/__init__.py index 48444e6..7e4da07 100644 --- a/typer/__init__.py +++ b/typer/__init__.py @@ -27,6 +27,7 @@ from click.utils import ( # noqa open_file, ) +from . import colors # noqa from .main import Typer, run # noqa from .models import ( # noqa Context, diff --git a/typer/colors.py b/typer/colors.py new file mode 100644 index 0000000..54e7b16 --- /dev/null +++ b/typer/colors.py @@ -0,0 +1,20 @@ +# Variable names to colors, just for completion +BLACK = "black" +RED = "red" +GREEN = "green" +YELLOW = "yellow" +BLUE = "blue" +MAGENTA = "magenta" +CYAN = "cyan" +WHITE = "white" + +RESET = "reset" + +BRIGHT_BLACK = "bright_black" +BRIGHT_RED = "bright_red" +BRIGHT_GREEN = "bright_green" +BRIGHT_YELLOW = "bright_yellow" +BRIGHT_BLUE = "bright_blue" +BRIGHT_MAGENTA = "bright_magenta" +BRIGHT_CYAN = "bright_cyan" +BRIGHT_WHITE = "bright_white"