📝 ✨ Add colors module and docs for printing and terminating, including tests.
This commit is contained in:
commit
051307093a
15 changed files with 402 additions and 0 deletions
15
docs/src/printing/tutorial001.py
Normal file
15
docs/src/printing/tutorial001.py
Normal 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)
|
9
docs/src/printing/tutorial002.py
Normal file
9
docs/src/printing/tutorial002.py
Normal 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)
|
25
docs/src/terminating/tutorial001.py
Normal file
25
docs/src/terminating/tutorial001.py
Normal 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)
|
12
docs/src/terminating/tutorial002.py
Normal file
12
docs/src/terminating/tutorial002.py
Normal 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)
|
12
docs/src/terminating/tutorial003.py
Normal file
12
docs/src/terminating/tutorial003.py
Normal 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)
|
|
@ -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
83
docs/tutorial/printing.md
Normal 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>
|
118
docs/tutorial/terminating.md
Normal file
118
docs/tutorial/terminating.md
Normal 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>
|
|
@ -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'
|
||||
|
|
0
tests/test_tutorial/test_terminating/__init__.py
Normal file
0
tests/test_tutorial/test_terminating/__init__.py
Normal file
35
tests/test_tutorial/test_terminating/test_tutorial001.py
Normal file
35
tests/test_tutorial/test_terminating/test_tutorial001.py
Normal 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
|
33
tests/test_tutorial/test_terminating/test_tutorial002.py
Normal file
33
tests/test_tutorial/test_terminating/test_tutorial002.py
Normal 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
|
34
tests/test_tutorial/test_terminating/test_tutorial003.py
Normal file
34
tests/test_tutorial/test_terminating/test_tutorial003.py
Normal 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
|
|
@ -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
20
typer/colors.py
Normal 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"
|
Loading…
Reference in a new issue