Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add colors module and docs for printing and terminating. #20

Merged
merged 4 commits into from
Jan 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/src/printing/tutorial001.py
Original file line number Diff line number Diff line change
@@ -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)
9 changes: 9 additions & 0 deletions docs/src/printing/tutorial002.py
Original file line number Diff line number Diff line change
@@ -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)
25 changes: 25 additions & 0 deletions docs/src/terminating/tutorial001.py
Original file line number Diff line number Diff line change
@@ -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)
12 changes: 12 additions & 0 deletions docs/src/terminating/tutorial002.py
Original file line number Diff line number Diff line change
@@ -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)
12 changes: 12 additions & 0 deletions docs/src/terminating/tutorial003.py
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 3 additions & 0 deletions docs/tutorial/first-steps.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
83 changes: 83 additions & 0 deletions docs/tutorial/printing.md
Original file line number Diff line number Diff line change
@@ -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>
118 changes: 118 additions & 0 deletions docs/tutorial/terminating.md
Original file line number Diff line number Diff line change
@@ -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>
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Empty file.
35 changes: 35 additions & 0 deletions tests/test_tutorial/test_terminating/test_tutorial001.py
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions tests/test_tutorial/test_terminating/test_tutorial002.py
Original file line number Diff line number Diff line change
@@ -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
Loading