📝 Add colors module and docs for printing and terminating, including tests.

This commit is contained in:
Sebastián Ramírez 2020-01-02 16:11:55 +01:00 committed by GitHub
commit 051307093a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 402 additions and 0 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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:

83
docs/tutorial/printing.md Normal file
View file

@ -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 <a href="https://click.palletsprojects.com/en/7.x/quickstart/#echoing" target="_blank">Click's docs</a>.
Check it:
<div class="termy">
```console
$ python main.py
Hello World
```
</div>
## Color
!!! info
For colors to work correctly on Windows you need to also install <a href="https://pypi.org/project/colorama/" target="_blank">`colorama`</a>.
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:
<div class="use-termynal" data-termynal>
<span data-ty="input">python main.py</span>
<span data-ty>everything is <span style="color: green; font-weight: bold;">good</span></span>
<span data-ty="input">python main.py --no-good</span>
<span data-ty>everything is <span style="color: white; background-color: red;">bad</span></span>
</div>
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 <a href="https://click.palletsprojects.com/en/7.x/api/#click.style" target="_blank">Click's docs about `style()`</a>
## `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:
<div class="use-termynal" data-termynal>
<span data-ty="input">python main.py Camila</span>
<span style="color: magenta;" data-ty>Welcome here Camila</span>
</div>

View file

@ -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:
<div class="termy">
```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
```
</div>
!!! 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:
<div class="termy">
```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.
```
</div>
!!! 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:
<div class="termy">
```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!
```
</div>

View file

@ -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'

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,

20
typer/colors.py Normal file
View file

@ -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"