walkingmask’s development log

IT系の情報などを適当に書いていきます

MENU

asdf, direnv で pyenv-virtualenv っぽい Python のローカル実行環境(macOS)を構築する

偉い人は言いました。

Python の開発環境は永遠に終わらない。なぜなら人類の夢だからだ。 アシスタ=レザー=ハーン(1975ー ) https://shindanmaker.com/364165

閑話休題

asdf, direnv のインストール

前提として、私は macOS のパッケージマネージャに Homebrew を、シェルに zsh を、zshプラグインマネージャに zhinit を使用しています。

まずは、asdf のインストール。

asdf は依存関係が簡単ではなさそうだったので、brew でインストールしちゃいます。

brew install asdf

その後、.zshrc に以下を記述。

# zinit の設定の後に

zinit ice lucid as'program' src'asdf.sh'
zinit load asdf-vm/asdf

次に direnv に関して .zshrc に記述します。

# direnv は zinit 公式の設定ですが、記述量がなかなか長い...

zinit ice lucid from'gh-r' as'program' mv'direnv* -> direnv' \
      atclone'./direnv hook zsh > zhook.zsh' atpull'%atclone' \
      pick'direnv' src='zhook.zsh'
zinit load direnv/direnv
export DIRENV_LOG_FORMAT=  # 標準出力を抑える
export DIRENV_WARN_TIMEOUT=1h  # venv 作成に時間が掛かると出てくる警告を抑える

ここまで書いて、zsh を再起動すると、direnv がインストールされ、asdf, direnv 共にパスが通っているはずです。

Python をインストールする

では、実際に pyenv-virtualenv っぽくしていきます。

まずは asdfPython をインストール。

asdf plugin add python
python_latest=$(asdf latest python)
asdf global python $python_latest system  # 2 つスペース区切りで指定すると、fallback してくれるらしい
asdf reshim python  # 一応

上記を実行すると ~/.tool-version が作成され、指定した Python が記録されているのを確認できます。

asdf reshim python の記述に関しては、asdf, asdf-python の issue にも上がっているのですが、Python の auto reshim がうまくできていない様子で、これに対する私の解決策は後述します。

"reshim" は 辞書で引くと "re-shim", "shim" は「ものを水平にしたり、すき間などに入れる)詰め木」の意味で、私は shim と symbolic link を掛け合わせて「シンボリックリンクを貼り直す」の意味だと解釈しています。

一旦、シェルを再起動して、

pytohn -V
# Python 3.9.5 と出力されれば OK(2021/05/15 現在)

次に仮想環境を作ります。

mkdir test_venv
cd test_venv
echo "layout python" >.envrc
direnv allow

これで仮想環境ができました。

試しにパッケージをインストールしてみます。

$ # test_venv で
$ pip install requests
(省略)
$ pip list 
Package    Version
---------- ---------
certifi    2020.12.5
chardet    4.0.0
idna       2.10
pip        21.1.1
requests   2.25.1
setuptools 56.0.0
urllib3    1.26.4
$ 
$ cd ..
$ # global な方の Python
$ pip list
Package    Version
---------- -------
pip        21.1.1
setuptools 56.0.0

確かにディレクトリ単位で Python 環境が分けられています。

また、cd することで自動で切り替わっています。

まあまあ pyenv-virtual に近い環境が作れたと思います。

いくつかの問題の対処

global な venv 作りたい

pyenv-virtualenv からの乗り換えで一番悩んだ問題です。

私は pyenv-virtualenv を以下のような使い方をしていました。

  1. pyenv virtualenv 3.8.5 playground
  2. pyenv global playground
  3. pip install numpy requests ...

この playground になんでも入れて、基本的にはこれを使い、ちょっと仮想環境を切りたいときには

  1. pyenv virtualenv 3.8.5 hoge
  2. pyenv local hoge
  3. pip install ...

としていたからです。

これに対する、私の解決策としては、

  1. asdf でインストールした Python を playground としてなんでも pip で入れ込む
  2. 仮想環境を切りたいときは venv を使う

これで、以前とほぼ同じ運用ができるはずです(というか、以前もこの運用でよかったと気づきました)。

他の解決法としては、ホームディレクトリ直下に .envrc を作ってしまうことが考えられます。

pip でインストールしたコマンドが使えない

具体手には以下のような例ですね。

$ pip install uvicorn
$ uvicorn --version
zsh: command not found: uvicorn

これは、上述した asdf の reshim が自動的に実行されない問題に起因すると思われます。

私の場合は、以下のように .zshrc に記述することで対処しました。

function __pip () {
  pip "$@"
  if [ $# -gt 0 ] && [ "$1" = "install" ]; then
    asdf reshim python
  fi
}
alias pip='__pip'

シンプルに pip install 後は reshim するようにしただけです。

イケてないですが、一旦よしとします。

venv 作成コマンド多すぎ

私は .zshrc に以下のようなコマンドを定義しています

function __venv () {
  [ ! "$VIRTUAL_ENV" ] && echo "layout python" >>${PWD}/.envrc && direnv allow || :
}
alias venv='__venv'

これで、特定ディレクトリ下で venv と実行するだけで良いです。

プロンプトの venv 名を変えられないの?

direnv の layout python には引数が渡せるので、

layout python pytohn3 --prompt venv_name

にすると、変えられるかもしれません。

私の場合、zsh のテーマに Powerline10k を使っていて、venv 下に入ると Python バージョンを勝手に表示してくれます。

ただ、逆に、上記の --prompt で渡した値は表示してくれません。

なぜなら、venv の activate は --prompt で渡した値を使って PS1 を書き換えるのですが、p10k は PS1 を常に書き換える& VIRTUAL_ENV 環境変数の値しか見てくれないからです。

その辺、色々ディスカッションはあるよう ですが、私の用途ではそこまで追求しないので、現状で良しとしました。

zsh テーマ、プロンプト拡張プラグインを使っていて、プロンプトに venv名を使いたい方は要注意ですね。

この辺は pyenv-virtualenv が恋しくなります(同様の問題が pyenv-virtualenv + p10k で発生しないとも言えませんが)。

シェル起動直後 venv のあるディレクトリにいるのに activate されない

私はとある経緯から、シェルを起動するときに最後に cd したパスで開くように設定しています。

if [[ -n $(echo ${^fpath}/chpwd_recent_dirs(N)) && -n $(echo ${^fpath}/cdr(N)) ]]; then
  autoload -Uz chpwd_recent_dirs cdr
  add-zsh-hook chpwd chpwd_recent_dirs
  zstyle ':chpwd:*' recent-dirs-max 100
  zstyle ':chpwd:*' recent-dirs-default true
  zstyle ':chpwd:*' recent-dirs-pushd true
fi
if [ -f ${ZDOTDIR}/.chpwd-recent-dirs ]; then
  cd $(head -1 ${ZDOTDIR}/.chpwd-recent-dirs | tr -d "$'")
  [ -f ${PWD}/.envrc ] && (direnv reload &) || :
fi

最後の fi の直前の direnv reload で、この課題は解決できました。

経緯

私の Python の開発環境において、以下を前提としています。

  • チーム共通で開発する場合は Docker を使う
  • 個人で開発するときも基本的に Docker を使う
  • エディタに VSCode を使っているので、devcontainer を使う
  • それでも、ローカルでサクッと実行したい時にローカル実行環境が欲しい

なので、ローカル Python はサクッと実行するときにしか使わないです。

ただ、以下のような要求を持っています。

  • requests や numpy など使用頻度の高いパッケージを global 環境に入れていたい
  • けど system python は汚したくない
  • なんとなくだけど仮想環境を分けたくなることはありそう

そのため、今までは

  • anyenv
  • pyenv
  • pyenv-virtualenv

を使っていました。pyenv-virtualenv は最高です。

ディレクトリ以下に .python-version があるときに、その環境に勝手に切り替えてくれます。

私は hoge/bin/activate や deactivate や pipenv shell が嫌いです。

しかし、OS アップデート&クリーンインストールした後に以下の問題にぶち当たりました。

github.com

シェルの設定ファイルにたった一行追加すれば良いことなのですが、anyenv は開発が活発ではありません。

今後、pyenv のメジャーアップデートごとに同じように対応が必要かもしれません。

「じゃあお前が anyenv に PR 出せば?」と言われても、私は弱々開発者で OSS フリーライダーなのです(今後は donation ぐらい最低限やっていきたいね)。

また、ついでに zsh 環境を見直したかったのと、新しいツールを触ってみたかったのです(これが本音だと思います、多分)。

まとめ

最終的には、anyenv pyenv virtualenv を使うよりも設定に手間が掛かっている気がします。

なので、やはり asdf を使ってみたかっただけなのだと思います。

というわけで、しばらくは asdf を使ってみようと思います。

もしかすると、また anyenv に戻るかもしれません。