CLIオプションの自動補完
ご覧のように、**Typer**で構築されたアプリは、Pythonパッケージを作成する場合やtyper
コマンドを使用する場合に、シェルで動作する補完機能を備えています。
通常、CLIオプション、CLI引数、およびサブコマンド(後ほど学習します)が補完されます。
しかし、CLIオプションとCLI引数の**値**についても自動補完を提供できます。ここではそれについて学習します。
補完の確認¶
カスタム補完の方法を確認する前に、もう一度その仕組みを確認しましょう。
独自のPythonパッケージの補完をインストールした後(またはtyper
コマンドを使用した後)、CLIプログラムを使用して--
で始まるCLIオプションを入力し始め、TABキーを押すと、シェルには使用可能なCLIオプションが表示されます(CLI引数などについても同様です)。
新しいPythonパッケージを作成せずにすばやく確認するには、typer
コマンドを使用します。
次に、小さな例題プログラムを作成しましょう
import typer
from typing_extensions import Annotated
app = typer.Typer()
@app.command()
def main(name: Annotated[str, typer.Option(help="The name to say hi to.")] = "World"):
print(f"Hello {name}")
if __name__ == "__main__":
app()
ヒント
可能であれば、Annotated
バージョンを使用することをお勧めします。
import typer
app = typer.Typer()
@app.command()
def main(name: str = typer.Option("World", help="The name to say hi to.")):
print(f"Hello {name}")
if __name__ == "__main__":
app()
そして、typer
コマンドを使用して補完を試してみましょう
// 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 --
$ typer ./main.py run --[TAB][TAB]
// You will get completion for --name, depending on your terminal it will look something like this
--name -- The name to say hi to.
// And you can run it as if it was with Python directly
$ typer ./main.py run --name Camila
Hello Camila
値のカスタム補完¶
現時点では、CLIオプション名については補完が得られますが、値については得られません。
CLIオプションコールバックとコンテキストのコールバック関数と同様に、autocompletion
関数を作成することで、値の補完を提供できます。
import typer
from typing_extensions import Annotated
def complete_name():
return ["Camila", "Carlos", "Sebastian"]
app = typer.Typer()
@app.command()
def main(
name: Annotated[
str, typer.Option(help="The name to say hi to.", autocompletion=complete_name)
] = "World",
):
print(f"Hello {name}")
if __name__ == "__main__":
app()
ヒント
可能であれば、Annotated
バージョンを使用することをお勧めします。
import typer
def complete_name():
return ["Camila", "Carlos", "Sebastian"]
app = typer.Typer()
@app.command()
def main(
name: str = typer.Option(
"World", help="The name to say hi to.", autocompletion=complete_name
),
):
print(f"Hello {name}")
if __name__ == "__main__":
app()
complete_name()
関数から文字列のlist
を返します。
そして、補完を使用するときにそれらの値を取得します。
$ typer ./main.py run --name [TAB][TAB]
// We get the values returned from the function 🎉
Camila Carlos Sebastian
基本的な動作を確認しました。次に、それを改善しましょう。
不完全な値の確認¶
現時点では、ユーザーがSebast
と入力してTABキーを押した場合でも、常にそれらの値を返します。そのため、Camila
やCarlos
の補完も得られます(シェルによって異なります)。しかし、Sebastian
の補完のみを得るべきです。
しかし、常に正しく動作するように修正できます。
complete_name()
関数を変更して、str
型の変数を受け取れるようにします。この変数には、不完全な値が含まれます。
次に、コマンドラインからの不完全な値で始まる値のみを確認して返します。
import typer
from typing_extensions import Annotated
valid_names = ["Camila", "Carlos", "Sebastian"]
def complete_name(incomplete: str):
completion = []
for name in valid_names:
if name.startswith(incomplete):
completion.append(name)
return completion
app = typer.Typer()
@app.command()
def main(
name: Annotated[
str, typer.Option(help="The name to say hi to.", autocompletion=complete_name)
] = "World",
):
print(f"Hello {name}")
if __name__ == "__main__":
app()
ヒント
可能であれば、Annotated
バージョンを使用することをお勧めします。
import typer
valid_names = ["Camila", "Carlos", "Sebastian"]
def complete_name(incomplete: str):
completion = []
for name in valid_names:
if name.startswith(incomplete):
completion.append(name)
return completion
app = typer.Typer()
@app.command()
def main(
name: str = typer.Option(
"World", help="The name to say hi to.", autocompletion=complete_name
),
):
print(f"Hello {name}")
if __name__ == "__main__":
app()
さあ、試してみましょう
$ typer ./main.py run --name Ca[TAB][TAB]
// We get the values returned from the function that start with Ca 🎉
Camila Carlos
これで、Ca
で始まる有効な値のみを返すようになりました。Sebastian
は補完オプションとして返されなくなりました。
ヒント
不完全な値をstr
型として宣言する必要があります。そして、関数でそれを受け取ります。
実際の値がint
型またはその他の型である場合でも、補完を行うときは、不完全な値としてstr
型のみを取得します。
同様に、int
型などではなく、str
型のみを返すことができます。
補完へのヘルプの追加¶
現時点では、str
型のlist
を返しています。
しかし、一部のシェル(Zsh、Fish、PowerShell)は、補完について追加のヘルプテキストを表示できます。
それらのシェルで追加のヘルプテキストを表示できるように、そのテキストを提供できます。
complete_name()
関数では、補完要素ごとに1つのstr
型を提供する代わりに、2つのアイテムを持つtuple
型を提供します。最初のアイテムは実際の補完文字列で、2番目のアイテムはヘルプテキストです。
したがって、最終的には、str
型のtuple
型のlist
を返します。
import typer
from typing_extensions import Annotated
valid_completion_items = [
("Camila", "The reader of books."),
("Carlos", "The writer of scripts."),
("Sebastian", "The type hints guy."),
]
def complete_name(incomplete: str):
completion = []
for name, help_text in valid_completion_items:
if name.startswith(incomplete):
completion_item = (name, help_text)
completion.append(completion_item)
return completion
app = typer.Typer()
@app.command()
def main(
name: Annotated[
str, typer.Option(help="The name to say hi to.", autocompletion=complete_name)
] = "World",
):
print(f"Hello {name}")
if __name__ == "__main__":
app()
ヒント
可能であれば、Annotated
バージョンを使用することをお勧めします。
import typer
valid_completion_items = [
("Camila", "The reader of books."),
("Carlos", "The writer of scripts."),
("Sebastian", "The type hints guy."),
]
def complete_name(incomplete: str):
completion = []
for name, help_text in valid_completion_items:
if name.startswith(incomplete):
completion_item = (name, help_text)
completion.append(completion_item)
return completion
app = typer.Typer()
@app.command()
def main(
name: str = typer.Option(
"World", help="The name to say hi to.", autocompletion=complete_name
),
):
print(f"Hello {name}")
if __name__ == "__main__":
app()
ヒント
各アイテムにヘルプテキストを含めたい場合は、リスト内の各アイテムがtuple
型であることを確認してください。list
型ではありません。
Clickは、ヘルプテキストを抽出するときに、tuple
型を特にチェックします。
したがって、最終的には、str
型のtuple
型のlist
(またはその他の反復可能オブジェクト)が返されます。
情報
ヘルプテキストは、Zsh、Fish、PowerShellで表示されます。
Bashはヘルプテキストの表示をサポートしませんが、補完は引き続き同じように動作します。
Zshなどのシェルを使用している場合、次のようになります。
$ typer ./main.py run --name [TAB][TAB]
// We get the completion items with their help text 🎉
Camila -- The reader of books.
Carlos -- The writer of scripts.
Sebastian -- The type hints guy.
yield
による簡素化¶
値(str
型またはtuple
型)を含むリストを作成して返す代わりに、補完に必要な各値にyield
を使用できます。
このようにして、関数はジェネレータになり、**Typer**(実際にはClick)が反復処理できます。
import typer
from typing_extensions import Annotated
valid_completion_items = [
("Camila", "The reader of books."),
("Carlos", "The writer of scripts."),
("Sebastian", "The type hints guy."),
]
def complete_name(incomplete: str):
for name, help_text in valid_completion_items:
if name.startswith(incomplete):
yield (name, help_text)
app = typer.Typer()
@app.command()
def main(
name: Annotated[
str, typer.Option(help="The name to say hi to.", autocompletion=complete_name)
] = "World",
):
print(f"Hello {name}")
if __name__ == "__main__":
app()
ヒント
可能であれば、Annotated
バージョンを使用することをお勧めします。
import typer
valid_completion_items = [
("Camila", "The reader of books."),
("Carlos", "The writer of scripts."),
("Sebastian", "The type hints guy."),
]
def complete_name(incomplete: str):
for name, help_text in valid_completion_items:
if name.startswith(incomplete):
yield (name, help_text)
app = typer.Typer()
@app.command()
def main(
name: str = typer.Option(
"World", help="The name to say hi to.", autocompletion=complete_name
),
):
print(f"Hello {name}")
if __name__ == "__main__":
app()
これにより、コードが少し簡素化され、同じように動作します。
ヒント
yield
の部分が複雑に思える場合は、心配しないでください。上記のlist
を使用したバージョンを使用できます。
結局のところ、これは単に数行のコードを節約するためです。
情報
関数はyield
を使用できるので、厳密にlist
型を返す必要はありません。反復可能であれば問題ありません。
しかし、補完の各要素は、str
型またはtuple
型(ヘルプテキストを含む場合)である必要があります。
コンテキストを使用したその他のCLIパラメータへのアクセス¶
複数の人物に同時に「こんにちは」と言うことができるようにプログラムを変更したいとしましょう。
そのため、複数の--name
CLIオプションを許可します。
ヒント
チュートリアルの後半で、複数値を持つCLIパラメータについて詳しく学習します。
そのため、今のところは、これはプレビューと考えてください😉。
これには、str
型のList
を使用します。
from typing import List
import typer
from typing_extensions import Annotated
app = typer.Typer()
@app.command()
def main(
name: Annotated[List[str], typer.Option(help="The name to say hi to.")] = ["World"],
):
for each_name in name:
print(f"Hello {each_name}")
if __name__ == "__main__":
app()
ヒント
可能であれば、Annotated
バージョンを使用することをお勧めします。
from typing import List
import typer
app = typer.Typer()
@app.command()
def main(name: List[str] = typer.Option(["World"], help="The name to say hi to.")):
for each_name in name:
print(f"Hello {each_name}")
if __name__ == "__main__":
app()
そして、次のように使用できます。
$ typer ./main.py run --name Camila --name Sebastian
Hello Camila
Hello Sebastian
複数値の補完の取得¶
以前と同様に、それらの名前の**補完**を提供したいと考えています。しかし、前のパラメータですでに与えられた**同じ名前**を補完に提供したくありません。
そのため、「コンテキスト」にアクセスして使用します。**Typer**アプリケーションを作成すると、内部でClickが使用されます。すべてのClickアプリケーションには、通常は非表示になっている"コンテキスト"と呼ばれる特別なオブジェクトがあります。
しかし、typer.Context
型の関数パラメータを宣言することで、コンテキストにアクセスできます。
そして、そのコンテキストから、各パラメータの現在の値を取得できます。
from typing import List
import typer
from typing_extensions import Annotated
valid_completion_items = [
("Camila", "The reader of books."),
("Carlos", "The writer of scripts."),
("Sebastian", "The type hints guy."),
]
def complete_name(ctx: typer.Context, incomplete: str):
names = ctx.params.get("name") or []
for name, help_text in valid_completion_items:
if name.startswith(incomplete) and name not in names:
yield (name, help_text)
app = typer.Typer()
@app.command()
def main(
name: Annotated[
List[str],
typer.Option(help="The name to say hi to.", autocompletion=complete_name),
] = ["World"],
):
for n in name:
print(f"Hello {n}")
if __name__ == "__main__":
app()
ヒント
可能であれば、Annotated
バージョンを使用することをお勧めします。
from typing import List
import typer
valid_completion_items = [
("Camila", "The reader of books."),
("Carlos", "The writer of scripts."),
("Sebastian", "The type hints guy."),
]
def complete_name(ctx: typer.Context, incomplete: str):
names = ctx.params.get("name") or []
for name, help_text in valid_completion_items:
if name.startswith(incomplete) and name not in names:
yield (name, help_text)
app = typer.Typer()
@app.command()
def main(
name: List[str] = typer.Option(
["World"], help="The name to say hi to.", autocompletion=complete_name
),
):
for n in name:
print(f"Hello {n}")
if __name__ == "__main__":
app()
この補完がトリガーされる前に、コマンドラインで--name
を使用して既に提供されたnames
を取得しています。
コマンドラインに--name
がない場合、None
になります。そのため、or []
を使用して、内容を後で確認するためのlist
(空の場合でも)があることを確認します。
次に、候補が揃ったら、各name
が--name
で既に指定されているかどうかを、name not in names
でnames
リストに存在するか確認します。
そして、まだ使用されていない各アイテムをyield
します。
確認してください
$ typer ./main.py run --name [TAB][TAB]
// The first time we trigger completion, we get all the names
Camila -- The reader of books.
Carlos -- The writer of scripts.
Sebastian -- The type hints guy.
// Add a name and trigger completion again
$ typer ./main.py run --name Sebastian --name Ca[TAB][TAB]
// Now we get completion only for the names we haven't used 🎉
Camila -- The reader of books.
Carlos -- The writer of scripts.
// And if we add another of the available names:
$ typer ./main.py run --name Sebastian --name Camila --name [TAB][TAB]
// We get completion for the only available one
Carlos -- The writer of scripts.
ヒント
選択肢が1つしかない場合、入力の手間を省くため、ヘルプテキスト付きの選択肢を表示する代わりに、シェルがすぐに補完する可能性があります。
生のCLIパラメータの取得¶
生のCLIパラメータ、つまり不完全な値の前にコマンドラインで渡されたすべての値を格納したstr
のlist
を取得することもできます。
例えば、["typer", "main.py", "run", "--name"]
のようなものになります。
ヒント
これは高度なシナリオの場合であり、ほとんどのユースケースではコンテキストを使用する方が良いでしょう。
しかし、必要であれば可能です。
簡単な例として、補完前に画面に表示してみましょう。
補完はプログラムが出力した結果に基づいているため(**Typer**によって内部的に処理されます)、補完中は通常どおり別のものを出力できません。
"標準エラー"への出力¶
ヒント
"標準出力"と"標準エラー"について復習する必要がある場合は、出力と色付け:「標準出力」と「標準エラー」のセクションを確認してください。
補完システムは"標準出力"からのみ読み取るため、"標準エラー"に出力しても補完が中断されることはありません。🚀
**Rich**のConsole(stderr=True)
を使用して"標準エラー"に出力できます。
stderr=True
を使用すると、**Rich**は出力を"標準エラー"に表示するよう指示します。
from typing import List
import typer
from rich.console import Console
from typing_extensions import Annotated
valid_completion_items = [
("Camila", "The reader of books."),
("Carlos", "The writer of scripts."),
("Sebastian", "The type hints guy."),
]
err_console = Console(stderr=True)
def complete_name(args: List[str], incomplete: str):
err_console.print(f"{args}")
for name, help_text in valid_completion_items:
if name.startswith(incomplete):
yield (name, help_text)
app = typer.Typer()
@app.command()
def main(
name: Annotated[
List[str],
typer.Option(help="The name to say hi to.", autocompletion=complete_name),
] = ["World"],
):
for n in name:
print(f"Hello {n}")
if __name__ == "__main__":
app()
ヒント
可能であれば、Annotated
バージョンを使用することをお勧めします。
from typing import List
import typer
from rich.console import Console
valid_completion_items = [
("Camila", "The reader of books."),
("Carlos", "The writer of scripts."),
("Sebastian", "The type hints guy."),
]
err_console = Console(stderr=True)
def complete_name(args: List[str], incomplete: str):
err_console.print(f"{args}")
for name, help_text in valid_completion_items:
if name.startswith(incomplete):
yield (name, help_text)
app = typer.Typer()
@app.command()
def main(
name: List[str] = typer.Option(
["World"], help="The name to say hi to.", autocompletion=complete_name
),
):
for n in name:
print(f"Hello {n}")
if __name__ == "__main__":
app()
情報
Richをインストールして使用できない場合は、print(lastname, file=sys.stderr)
またはtyper.echo("some text", err=True)
を使用することもできます。
List[str]
型の(ここではargs
という名前の)パラメータを宣言することで、生のstr
のlist
としてすべてのCLIパラメータを取得します。
ヒント
Clickとの慣例に従って、すべての生のCLIパラメータのリストにargs
という名前を付けています。
しかし、これはCLI引数だけでなく、CLIオプションと値もすべて含む生のstr
のlist
です。
そして、それを"標準エラー"に出力します。
$ typer ./main.py run --name [TAB][TAB]
// First we see the raw CLI parameters
['./main.py', 'run', '--name']
// And then we see the actual completion
Camila -- The reader of books.
Carlos -- The writer of scripts.
Sebastian -- The type hints guy.
ヒント
これは非常に単純な(そしてかなり役に立たない)例ですが、その仕組みと使用方法を知るためです。
しかし、これは非常に高度なユースケースでのみ役立つ可能性があります。
コンテキストと生のCLIパラメータの取得¶
もちろん、必要であればすべてを宣言できます。コンテキスト、生のCLIパラメータ、そして不完全なstr
です。
from typing import List
import typer
from rich.console import Console
from typing_extensions import Annotated
valid_completion_items = [
("Camila", "The reader of books."),
("Carlos", "The writer of scripts."),
("Sebastian", "The type hints guy."),
]
err_console = Console(stderr=True)
def complete_name(ctx: typer.Context, args: List[str], incomplete: str):
err_console.print(f"{args}")
names = ctx.params.get("name") or []
for name, help_text in valid_completion_items:
if name.startswith(incomplete) and name not in names:
yield (name, help_text)
app = typer.Typer()
@app.command()
def main(
name: Annotated[
List[str],
typer.Option(help="The name to say hi to.", autocompletion=complete_name),
] = ["World"],
):
for n in name:
print(f"Hello {n}")
if __name__ == "__main__":
app()
ヒント
可能であれば、Annotated
バージョンを使用することをお勧めします。
from typing import List
import typer
from rich.console import Console
valid_completion_items = [
("Camila", "The reader of books."),
("Carlos", "The writer of scripts."),
("Sebastian", "The type hints guy."),
]
err_console = Console(stderr=True)
def complete_name(ctx: typer.Context, args: List[str], incomplete: str):
err_console.print(f"{args}")
names = ctx.params.get("name") or []
for name, help_text in valid_completion_items:
if name.startswith(incomplete) and name not in names:
yield (name, help_text)
app = typer.Typer()
@app.command()
def main(
name: List[str] = typer.Option(
["World"], help="The name to say hi to.", autocompletion=complete_name
),
):
for n in name:
print(f"Hello {n}")
if __name__ == "__main__":
app()
確認してください
$ typer ./main.py run --name [TAB][TAB]
// First we see the raw CLI parameters
['./main.py', 'run', '--name']
// And then we see the actual completion
Camila -- The reader of books.
Carlos -- The writer of scripts.
Sebastian -- The type hints guy.
$ typer ./main.py run --name Sebastian --name Ca[TAB][TAB]
// Again, we see the raw CLI parameters
['./main.py', 'run', '--name', 'Sebastian', '--name']
// And then we see the rest of the valid completion items
Camila -- The reader of books.
Carlos -- The writer of scripts.
いたるところに型¶
**Typer**は型宣言を使用して、autocompletion
関数に提供する必要があるものを検出します。
これらの型の関数パラメータを宣言できます。
str
:不完全な値用。typer.Context
:現在のコンテキスト用。List[str]
:生のCLIパラメータ用。
名前、順序、3つのオプションのどれを宣言するかは関係ありません。すべて「正常に動作します」✨