はじめに
この記事は ZOZOテクノロジーズ #3 Advent Calendar 2020 の19日目の記事です。
昨日の記事は @BrunoB さんによる PAC4J : Security library for Play framework and Scala でした。
TL;DR
- Python のコマンドライン引数で渡したファイルパスと同じ階層に
.python-versionが存在する場合、 Python のバージョンが切り替わってしまう挙動を確認 - ファイルパスと同じ階層にある
.python-versionを削除するか、 環境変数PYENV_VERSIONを設定することで解決
問題の挙動
初めて挙動に違和感を覚えたのは、python hoge.py -a ./fuga/piyo.txtコマンド実行時に、ModuleNotFoundErrorが出力された時です。
そのライブラリがインストールされていることはpip listコマンドで確認済だったので、上記コマンドでエラーが出力される理由が分かりませんでした。
詳しく調べてみると、pyenv localコマンドで指定したバージョンの Python が実行されず、システムデフォルトの Python が実行されており、システムデフォルトの Python には上記ライブラリがインストールされていないため、ModuleNotFoundErrorが発生していました。
どうやら、コマンドライン引数で渡した./fuga/piyo.txtと同じ階層にある.python-versionが読み込まれて、実行されるPythonのバージョンが切り替わっているようでした。
問題となる挙動の再現
以下の環境、コード、コマンドで問題の挙動を再現します。
環境
AWS EC2 のインスタンスを用います。
- OS:Amazon Linux 2 AMI (HVM), SSD Volume Type
- pyenv のバージョン:1.2.21(2020/12/19時点の最新バージョン)
- システムデフォルトの Python のバージョン:2.7.18
- pyenv で入れた Python のバージョン:3.7.6
コード
今回実行するコードprint_version.pyの中身は以下の通りです。引数arg1の内容に関係なく、実行された Python のバージョンをprint(sys.version)で表示するものです。
引数arg1はmain関数内で使われませんが、この存在が今回のキモになります。
import argparse
import sys
def main(arg1):
print(sys.version)
return()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("-a", "--arg1")
args = parser.parse_args()
main(args.arg1)
そして、ディレクトリ構造は以下の通りです。
print_version.pyと同じ階層に2個のディレクトリがあり、それらの中身は.python-versionの有無以外が同じです。
ちなみに、.python-versionはpyenv localコマンドが実行されたディレクトリに作成される隠しファイルなので、後述しますがpyenv-testディレクトリでpyenv local 3.7.6コマンドを、with-pvディレクトリでpyenv local systemコマンドを実行しています。
pyenv-test
├── .python-version
├── print_version.py
├── with-pv
│ ├── .python-version
│ └── a.txt
└── without-pv
└── a.txt
3.7.6
system
abcd
コマンド
まずは以下のコマンドを実行し、出力結果も載せます。
[ec2-user@******** pyenv-test]$ cd with-pv/
[ec2-user@******** with-pv]$ pyenv local system
[ec2-user@******** with-pv]$ python -V
Python 2.7.18
[ec2-user@******** with-pv]$ cd ../
[ec2-user@******** pyenv-test]$ pyenv local 3.7.6
[ec2-user@******** pyenv-test]$ python -V
Python 3.7.6
引き続き以下のコマンドを実行し、出力結果も載せます。
繰り返しになりますが、print_version.pyは実行された Python のバージョンを表示するだけのコードです。
[ec2-user@******** pyenv-test]$ python print_version.py -a ./without-pv/a.txt
3.7.6 (default, Dec 8 2020, 06:20:44)
[GCC 7.3.1 20180712 (Red Hat 7.3.1-9)]
[ec2-user@******** pyenv-test]$ python print_version.py -a ./with-pv/a.txt
2.7.18 (default, Aug 27 2020, 21:22:52)
[GCC 7.3.1 20180712 (Red Hat 7.3.1-9)]
中身が同じのファイルをprint_version.pyの引数として渡したはずですが、実行された Python のバージョンが異なっています。
./with-pv/a.txtをコマンドライン引数として渡した方は、pyenv で指定していないはずの Python 2.7.18 が実行されました。
両コマンドの違いは、コマンドライン引数として渡したファイルパスと同じ階層に、.python-versionが有るか否かで、.python-versionに記載された Python のバージョンが優先して実行されていると考えられます。
ちなみに、argparse の挙動による可能性も考えられるため、Click でも同様のことを試しましたが、結果は同じでした。
解決策
以下の2通りの方法があります。
1:環境変数PYENV_VERSIONを設定する
実行される Python のバージョンを pyenv が決定する際に、最も優先されるのは環境変数PYENV_VERSIONです(参考:pyenv の本家リポジトリ)。
export PYENV_VERSION=3.7.6コマンドを実行するか、pyenv shell 3.7.6コマンドを実行することでその環境変数を設定でき、実行したのが以下のものです。
[ec2-user@******** pyenv-test]$ pyenv shell 3.7.6
[ec2-user@******** pyenv-test]$ python print_version.py -a ./without-pv/a.txt
3.7.6 (default, Dec 8 2020, 06:20:44)
[GCC 7.3.1 20180712 (Red Hat 7.3.1-9)]
[ec2-user@******** pyenv-test]$ python print_version.py -a ./with-pv/a.txt
3.7.6 (default, Dec 8 2020, 06:20:44)
[GCC 7.3.1 20180712 (Red Hat 7.3.1-9)]
2:.python-versionを削除する
コマンドの節で確認した通り、引数でファイルと同じ階層に.python-versionが存在しなければ、pyenv localコマンドで指定された Python のバージョンで実行されます。
考察
pyenv の本家リポジトリによると、コマンドライン引数で指定したファイルと同じ階層に存在する.python-versionは、実行される Python のバージョンに影響を及ぼさないことが期待されます。
しかし、期待通りの挙動が見られなかったため、より詳しく仕様を把握する必要がありそうです。
おわりに
自分が pyenv を使用している時にハマった箇所を本記事にまとめました。
ModuleNotFoundErrorの時点では、Python のバージョンが切り替わっていることに全く気が付かず、pyenv のインストールに失敗したことを長い間疑っており、解決まで時間がかかってしまいました。
更に、本記事ではコマンドライン引数が2個以上の場合を割愛しておりますが、私が当時書いていたコードはコマンドライン引数が4個ありました。これも解決まで時間がかかった要員の1つですが、なんとか原因を特定することができました。
明日は @Horie1024 さんの記事となります。明日もお楽しみに。
追記(2021/4/8)
実は 2021/2/8 に本記事の内容で以下の issue を立てました。
https://github.com/pyenv/pyenv/issues/1807
この結果、少なくとも pyenv のバージョンが 1.2.23(2021/4/8時点の最新バージョン)では上記挙動が見られなくなりました。