File: callback-and-context.md

package info (click to toggle)
typer 0.19.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,688 kB
  • sloc: python: 16,702; javascript: 280; sh: 28; makefile: 27
file content (211 lines) | stat: -rw-r--r-- 6,950 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# CLI Option Callback and Context

In some occasions you might want to have some custom logic for a specific *CLI parameter* (for a *CLI option*  or *CLI argument*) that is executed with the value received from the terminal.

In those cases you can use a *CLI parameter* callback function.

## Validate *CLI parameters*

For example, you could do some validation before the rest of the code is executed.

{* docs_src/options/callback/tutorial001_an.py hl[7:10,13] *}

Here you pass a function to `typer.Option()` or `typer.Argument()` with the keyword argument `callback`.

The function receives the value from the command line. It can do anything with it, and then return the value.

In this case, if the `--name` is not `Camila` we raise a `typer.BadParameter()` exception.

The `BadParameter` exception is special, it shows the error with the parameter that generated it.

Check it:

<div class="termy">

```console
$ python main.py --name Camila

Hello Camila

$ python main.py --name Rick

Usage: main.py [OPTIONS]

// We get the error from the callback
Error: Invalid value for '--name': Only Camila is allowed
```

</div>

## Handling completion

There's something to be aware of with callbacks and completion that requires some small special handling.

But first let's just use completion in your shell (Bash, Zsh, Fish, or PowerShell).

After installing completion (for your own Python package), when you use your CLI program and start adding a *CLI option* with `--` and then hit <kbd>TAB</kbd>, your shell will show you the available *CLI options* (the same for *CLI arguments*, etc).

To check it quickly with the previous script use the `typer` command:

<div class="termy">

```console
// Hit the TAB key in your keyboard below where you see the: [TAB]
$ typer ./main.py [TAB][TAB]

// Depending on your terminal/shell you will get some completion like this ✨
run    -- Run the provided Typer app.
utils  -- Extra utility commands for Typer apps.

// Then try with "run" and --help
$ typer ./main.py run --help

// You get a help text with your CLI options as you normally would
Usage: typer run [OPTIONS]

  Run the provided Typer app.

Options:
  --name TEXT  [required]
  --help       Show this message and exit.

// Then try completion with your program
$ typer ./main.py run --[TAB][TAB]

// You get completion for CLI options
--help  -- Show this message and exit.
--name

// And you can run it as if it was with Python directly
$ typer ./main.py run --name Camila

Hello Camila
```

</div>

### How shell completion works

The way it works internally is that the shell/terminal will call your CLI program with some special environment variables (that hold the current *CLI parameters*, etc) and your CLI program will print some special values that the shell will use to present completion. All this is handled for you by **Typer** behind the scenes.

But the main **important point** is that it is all based on values printed by your program that the shell reads.

### Breaking completion in a callback

Let's say that when the callback is running, we want to show a message saying that it's validating the name:

{* docs_src/options/callback/tutorial002_an.py hl[8] *}

And because the callback will be called when the shell calls your program asking for completion, that message `"Validating name"` will be printed and it will break completion.

It will look something like:

<div class="termy">

```console
// Run it normally
$ typer ./main.py run --name Camila

// See the extra message "Validating name"
Validating name
Hello Camila

$ typer ./main.py run --[TAB][TAB]

// Some weird broken error message ⛔️
(eval):1: command not found: Validating
rutyper ./main.pyed Typer app.
```

</div>

### Fix completion - using the `Context`

When you create a **Typer** application it uses Click underneath.

And every Click application has a special object called a <a href="https://click.palletsprojects.com/en/7.x/commands/#nested-handling-and-contexts" class="external-link" target="_blank">"Context"</a> that is normally hidden.

But you can access the context by declaring a function parameter of type `typer.Context`.

The "context" has some additional data about the current execution of your program:

{* docs_src/options/callback/tutorial003_an.py hl[7:9] *}

The `ctx.resilient_parsing` will be `True` when handling completion, so you can just return without printing anything else.

But it will be `False` when calling the program normally. So you can continue the execution of your previous code.

That's all is needed to fix completion. 🚀

Check it:

<div class="termy">

```console
$ typer ./main.py run --[TAB][TAB]

// Now it works correctly 🎉
--help  -- Show this message and exit.
--name

// And you can call it normally
$ typer ./main.py run --name Camila

Validating name
Hello Camila
```

</div>

## Using the `CallbackParam` object

The same way you can access the `typer.Context` by declaring a function parameter with its value, you can declare another function parameter with type `typer.CallbackParam` to get the specific Click `Parameter` object.

{* docs_src/options/callback/tutorial004_an.py hl[7,10] *}

It's probably not very common, but you could do it if you need it.

For example if you had a callback that could be used by several *CLI parameters*, that way the callback could know which parameter is each time.

Check it:

<div class="termy">

```console
$ python main.py --name Camila

Validating param: name
Hello Camila
```

</div>

## Technical Details

Because you get the relevant data in the callback function based on standard Python type annotations, you get type checks and autocompletion in your editor for free.

And **Typer** will make sure you get the function parameters you want.

You don't have to worry about their names, their order, etc.

As it's based on standard Python types, it "**just works**". ✨

### Click's `Parameter`

The `typer.CallbackParam` is actually just a sub-class of Click's <a href="https://click.palletsprojects.com/en/7.x/api/#click.Parameter" class="external-link" target="_blank">`Parameter`</a>, so you get all the right completion in your editor.

### Callback with type annotations

You can get the `typer.Context` and the `typer.CallbackParam` simply by declaring a function parameter of each type.

The order doesn't matter, the name of the function parameters doesn't matter.

You could also get only the `typer.CallbackParam` and not the `typer.Context`, or vice versa, it will still work.

### `value` function parameter

The `value` function parameter in the callback can also have any name (e.g. `lastname`) and any type, but it should have the same type annotation as in the main function, because that's what it will receive.

It's also possible to not declare its type. It will still work.

And it's possible to not declare the `value` parameter at all, and, for example, only get the `typer.Context`. That will also work.