2024年1月9日火曜日

pipenv の zsh 補完

Django の開発中の不満の一つが pipenv run の補完だ。 zsh は補完が凄いと聞いていたのに bash から移行してストレスが増えた。 bash はとりあえずファイルがそこにあればいつでも補完が効く。 pipenv run python と打った後にはカレントの manage.py が補完できる。 それに対して、zsh の補完はもっと inteligent で文脈を理解するのだが、何の指定もない文脈では何も補完できない。 本当に使えない。 まあお気づきかと思うが、zsh が使えないというより設定ファイルが酷い、という話、のはずだ。

調査

まずは状況を確認しよう。 Mac の homebrew で pipenv を入れていて、シェルは zsh。 .zshrc で補完の設定は最低限のものだけ。

autoload -Uz compinit && compinit
zstyle ':completion:*' completer _complete _ignored _files
.zshrc

pipenv の補完設定は /usr/local/share/zsh/site-functions/_pipenv にあるが、これは /usr/local/Cellar/pipenv/2023.11.15/share/zsh/site-functions/_pipenv へのシンボリックリンクだ。 homebrew がこの設定ファイルをどこから持ってきているかというと、pipenv.rb の中で次の補助関数で生成している。

generate_completions_from_executable(libexec/"bin/pipenv", shells:                 [:fish, :zsh],
                                                           shell_parameter_format: :click)
pipenv.rb

generate_completions_from_executable は、formula.rb に定義がある。 shell_parameter_format を :click にして shell に :zsh を渡すと最終的に env _PIPENV_COMPLETE=zsh_source pipenv というような呼び出しが行われる。 pipenv のドキュメントにある eval "$(_PIPENV_COMPLETE=zsh_source pipenv)" にたどり着くのだ。 つまり、pipenv 側が用意した設定だと言って良い。

click とは何ぞや?

Click is a Python package for creating beautiful command line interfaces in a composable way with as little code as necessary. It’s the “Command Line Interface Creation Kit”. It’s highly configurable but comes with sensible defaults out of the box.

Welcome to Click

Python で CLI を作るときのツールキットらしい。 シェルの補完設定も作れる(Shell Completion)。 つまり、pipenv はこれを利用して作られている、と。 コードを確認してみよう。

@cli.command(
    short_help="Spawns a command installed into the virtualenv.",
    context_settings=subcommand_context_no_interspersion,
)
@common_options
@argument("command")
@argument("args", nargs=-1)
@pass_state
def run(state, command, args):
    """Spawns a command installed into the virtualenv."""
    from pipenv.routines.shell import do_run

    do_run(
        state.project,
        command=command,
        args=args,
        python=state.python,
        pypi_mirror=state.pypi_mirror,
    )
pipenv/cli/command.py

このデコレーターたちが click のもので、デコレーターの引数によって補完の挙動が変わるようだ(詳細は把握していない)。 ざっとドキュメントを見た感じ、残りはただのコマンドラインみたいな指定ができる方法が見当たらない。 ということでこの仕組みのまま直す方法は無さそう…。

解決

pipenv に手を入れて解決するのは難しそうなので、zsh の補完で pipenv run にだけ適用するパターンを作りたい。 遠い昔に買った「zshの本」に nice などに適用される方法が載っていたのでそれを参考にする。

#compdef -p pipenv

local cur in_run
cur=$CURRENT
in_run=0
while (( cur - 2 )) do
    if [[ $words[$(( $cur - 1 ))] == "run" ]]; then
        in_run=1
        break
    fi
    (( cur-- ))
done
if [[ $in_run == 1 ]]; then
    while (( cur - 1 )) do
        shift words
        (( CURRENT-- ))
        (( cur-- ))
    done
    _normal
else
    _pipenv
fi

大雑把に説明すると、pipenv の引数に run が入っている場合に、それより前の(グローバルオプションなども含む)部分を無視した文脈で補完し直す、という方針になる。

注意点1:「pipenv run にだけ適用」を zsh 側で書けなかったので、pipenv に適用する補完関数として、run が入ってなければ _pipenv での補完に戻る、という書き方をしている。

注意点2: 先頭行の「#compdef -p pipenv」は、"#compdef pipenv" と書いたら pipenv に対する補完の定義、ということになると思うのだが、既に _pipenv が定義されていて二つめは受け付けてもらえないようだったので、ファイルパターン(glob パターン)で指定した。

あとはこのファイルを /some/where/_pipenv_run としておいて、compinit の前で fpath に /some/where を追加したら、起動時に読み込んでもらえる。

もう少し効率よく書けたらいいと思うのだが、ひとまずはこれで良しとしよう。

2024年1月2日火曜日

2023年の読書

読書メーターの記録によるとマンガを入れて78冊。 まあ去年と大差ない。

印象に残っているのは 「紋章学入門」「神田神保町書肆街考」という厚めのちくま学芸文庫、 「運動しても痩せないのはなぜか」 辺り。 数学では「グレブナー基底と代数多様体入門」は良い本だった。表現論の本には手を出して挫折、を繰り返していた感じ。 マンガでは継続して読んでいた「プリニウス」「重版出来」「ダンジョン飯」が完結したし、「フットボールネーション」や「アルテ」はそろそろ終わりそうな雰囲気になってきたので、この先は読む量がだんだん減りそう…いや「ぼっち・ざ・ろっく!」とか読み始めてしまったし、そんなことはないか。 YouTube きっかけは「冬虫夏草ハンドブック」があった。うごめ紀という生物系のチャンネルで紹介されていたもの。