コンテンツへスキップ

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キーを押した場合でも、常にそれらの値を返します。そのため、CamilaCarlosの補完も得られます(シェルによって異なります)。しかし、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 namesnamesリストに存在するか確認します。

そして、まだ使用されていない各アイテムを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パラメータ、つまり不完全な値の前にコマンドラインで渡されたすべての値を格納したstrlistを取得することもできます。

例えば、["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という名前の)パラメータを宣言することで、生のstrlistとしてすべてのCLIパラメータを取得します。

ヒント

Clickとの慣例に従って、すべての生のCLIパラメータのリストにargsという名前を付けています。

しかし、これはCLI引数だけでなく、CLIオプションと値もすべて含む生のstrlistです。

そして、それを"標準エラー"に出力します。

$ 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つのオプションのどれを宣言するかは関係ありません。すべて「正常に動作します」✨